restertet yazi setup

new file:   herdr/.config/herdr/sessions/home/session-history.json
	new file:   herdr/.config/herdr/sessions/home/session.json
	new file:   kanata/.config/systemd/user/default.target.wants/duplicati.service
	new file:   pi/.pi/agent/extensions/atuin.ts
	new file:   yazi/.config/yazi/bookmark
	modified:   yazi/.config/yazi/init.lua
	modified:   yazi/.config/yazi/keymap.toml
	modified:   yazi/.config/yazi/package.toml
	renamed:    yazi/.config/yazi/flavors/catppuccin-mocha.yazi/LICENSE -> yazi/.config/yazi/plugins/chmod.yazi/LICENSE
	new file:   yazi/.config/yazi/plugins/chmod.yazi/README.md
	new file:   yazi/.config/yazi/plugins/chmod.yazi/main.lua
	new file:   yazi/.config/yazi/plugins/eza-preview.yazi/LICENSE
	new file:   yazi/.config/yazi/plugins/eza-preview.yazi/README.md
	new file:   yazi/.config/yazi/plugins/eza-preview.yazi/main.lua
	modified:   yazi/.config/yazi/plugins/fchar.yazi/README.md
	modified:   yazi/.config/yazi/plugins/fchar.yazi/main.lua
	deleted:    yazi/.config/yazi/plugins/file-extra-metadata.yazi/main.lua
	modified:   yazi/.config/yazi/plugins/mediainfo.yazi/LICENSE
	modified:   yazi/.config/yazi/plugins/mediainfo.yazi/README.md
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/adobe-old.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/adobe-stable.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/audio-old.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/audio-stable.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/const.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/image-old.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/image-stable.lua
	modified:   yazi/.config/yazi/plugins/mediainfo.yazi/main.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/none-media-preview-old.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/none-media-preview-stable.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/utils.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/video-old.lua
	new file:   yazi/.config/yazi/plugins/mediainfo.yazi/video-stable.lua
	modified:   yazi/.config/yazi/plugins/mime-ext.yazi/README.md
	new file:   yazi/.config/yazi/plugins/mime-ext.yazi/local.lua
	modified:   yazi/.config/yazi/plugins/mime-ext.yazi/main.lua
	new file:   yazi/.config/yazi/plugins/mime-ext.yazi/remote.lua
	modified:   yazi/.config/yazi/plugins/rich-preview.yazi/main.lua
	modified:   yazi/.config/yazi/plugins/smart-enter.yazi/README.md
	deleted:    yazi/.config/yazi/plugins/whoosh.yazi/.luarc.json
	modified:   yazi/.config/yazi/plugins/whoosh.yazi/LICENSE
	deleted:    yazi/.config/yazi/plugins/whoosh.yazi/README-RU.md
	modified:   yazi/.config/yazi/plugins/whoosh.yazi/README.md
	modified:   yazi/.config/yazi/plugins/whoosh.yazi/bookmarks
	deleted:    yazi/.config/yazi/plugins/whoosh.yazi/image/history.png
	deleted:    yazi/.config/yazi/plugins/whoosh.yazi/image/plugin.png
	modified:   yazi/.config/yazi/plugins/whoosh.yazi/main.lua
	modified:   yazi/.config/yazi/yazi.toml
	renamed:    yazi/.config/yazi/plugins/full-border.yazi/LICENSE -> yazi_1/.config/yazi/flavors/catppuccin-mocha.yazi/LICENSE
	renamed:    yazi/.config/yazi/flavors/catppuccin-mocha.yazi/LICENSE-tmtheme -> yazi_1/.config/yazi/flavors/catppuccin-mocha.yazi/LICENSE-tmtheme
	renamed:    yazi/.config/yazi/flavors/catppuccin-mocha.yazi/README.md -> yazi_1/.config/yazi/flavors/catppuccin-mocha.yazi/README.md
	renamed:    yazi/.config/yazi/flavors/catppuccin-mocha.yazi/flavor.toml -> yazi_1/.config/yazi/flavors/catppuccin-mocha.yazi/flavor.toml
	renamed:    yazi/.config/yazi/flavors/catppuccin-mocha.yazi/preview.png -> yazi_1/.config/yazi/flavors/catppuccin-mocha.yazi/preview.png
	renamed:    yazi/.config/yazi/flavors/catppuccin-mocha.yazi/tmtheme.xml -> yazi_1/.config/yazi/flavors/catppuccin-mocha.yazi/tmtheme.xml
	renamed:    yazi/.config/yazi/flavors/dracula.yazi -> yazi_1/.config/yazi/flavors/dracula.yazi
	renamed:    yazi/.config/yazi/flavors/everforest-medium.yazi/LICENSE -> yazi_1/.config/yazi/flavors/everforest-medium.yazi/LICENSE
	renamed:    yazi/.config/yazi/flavors/everforest-medium.yazi/LICENSE-tmtheme -> yazi_1/.config/yazi/flavors/everforest-medium.yazi/LICENSE-tmtheme
	renamed:    yazi/.config/yazi/flavors/everforest-medium.yazi/README.md -> yazi_1/.config/yazi/flavors/everforest-medium.yazi/README.md
	renamed:    yazi/.config/yazi/flavors/everforest-medium.yazi/flavor.toml -> yazi_1/.config/yazi/flavors/everforest-medium.yazi/flavor.toml
	renamed:    yazi/.config/yazi/flavors/everforest-medium.yazi/preview.png -> yazi_1/.config/yazi/flavors/everforest-medium.yazi/preview.png
	renamed:    yazi/.config/yazi/flavors/everforest-medium.yazi/tmtheme.xml -> yazi_1/.config/yazi/flavors/everforest-medium.yazi/tmtheme.xml
	new file:   yazi_1/.config/yazi/flavors/gruvbox-dark.yazi/LICENSE
	new file:   yazi_1/.config/yazi/flavors/gruvbox-dark.yazi/LICENSE-tmtheme
	new file:   yazi_1/.config/yazi/flavors/gruvbox-dark.yazi/README.md
	new file:   yazi_1/.config/yazi/flavors/gruvbox-dark.yazi/flavor.toml
	new file:   yazi_1/.config/yazi/flavors/gruvbox-dark.yazi/preview.png
	new file:   yazi_1/.config/yazi/flavors/gruvbox-dark.yazi/tmtheme.xml
	renamed:    yazi/.config/yazi/flavors/gruvbox-material.yazi/LICENSE -> yazi_1/.config/yazi/flavors/gruvbox-material.yazi/LICENSE
	renamed:    yazi/.config/yazi/flavors/gruvbox-material.yazi/LICENSE-tmtheme -> yazi_1/.config/yazi/flavors/gruvbox-material.yazi/LICENSE-tmtheme
	renamed:    yazi/.config/yazi/flavors/gruvbox-material.yazi/README.md -> yazi_1/.config/yazi/flavors/gruvbox-material.yazi/README.md
	renamed:    yazi/.config/yazi/flavors/gruvbox-material.yazi/flavor.toml -> yazi_1/.config/yazi/flavors/gruvbox-material.yazi/flavor.toml
	renamed:    yazi/.config/yazi/flavors/gruvbox-material.yazi/preview.png -> yazi_1/.config/yazi/flavors/gruvbox-material.yazi/preview.png
	renamed:    yazi/.config/yazi/flavors/gruvbox-material.yazi/tmtheme.xml -> yazi_1/.config/yazi/flavors/gruvbox-material.yazi/tmtheme.xml
	renamed:    yazi/.config/yazi/flavors/kanagawa.yazi/LICENSE -> yazi_1/.config/yazi/flavors/kanagawa.yazi/LICENSE
	renamed:    yazi/.config/yazi/flavors/kanagawa.yazi/LICENSE-tmtheme -> yazi_1/.config/yazi/flavors/kanagawa.yazi/LICENSE-tmtheme
	renamed:    yazi/.config/yazi/flavors/kanagawa.yazi/README.md -> yazi_1/.config/yazi/flavors/kanagawa.yazi/README.md
	renamed:    yazi/.config/yazi/flavors/kanagawa.yazi/flavor.toml -> yazi_1/.config/yazi/flavors/kanagawa.yazi/flavor.toml
	renamed:    yazi/.config/yazi/flavors/kanagawa.yazi/preview.png -> yazi_1/.config/yazi/flavors/kanagawa.yazi/preview.png
	renamed:    yazi/.config/yazi/flavors/kanagawa.yazi/tmtheme.xml -> yazi_1/.config/yazi/flavors/kanagawa.yazi/tmtheme.xml
	renamed:    yazi/.config/yazi/flavors/monokai.yazi/LICENSE -> yazi_1/.config/yazi/flavors/monokai.yazi/LICENSE
	renamed:    yazi/.config/yazi/flavors/monokai.yazi/LICENSE-tmtheme -> yazi_1/.config/yazi/flavors/monokai.yazi/LICENSE-tmtheme
	renamed:    yazi/.config/yazi/flavors/monokai.yazi/README.md -> yazi_1/.config/yazi/flavors/monokai.yazi/README.md
	renamed:    yazi/.config/yazi/flavors/monokai.yazi/flavor.toml -> yazi_1/.config/yazi/flavors/monokai.yazi/flavor.toml
	renamed:    yazi/.config/yazi/flavors/monokai.yazi/preview.png -> yazi_1/.config/yazi/flavors/monokai.yazi/preview.png
	renamed:    yazi/.config/yazi/flavors/monokai.yazi/tmtheme.xml -> yazi_1/.config/yazi/flavors/monokai.yazi/tmtheme.xml
	renamed:    yazi/.config/yazi/flavors/rose-pine-moon.yazi/LICENSE -> yazi_1/.config/yazi/flavors/rose-pine-moon.yazi/LICENSE
	renamed:    yazi/.config/yazi/flavors/rose-pine-moon.yazi/LICENSE-tmtheme -> yazi_1/.config/yazi/flavors/rose-pine-moon.yazi/LICENSE-tmtheme
	renamed:    yazi/.config/yazi/flavors/rose-pine-moon.yazi/README.md -> yazi_1/.config/yazi/flavors/rose-pine-moon.yazi/README.md
	renamed:    yazi/.config/yazi/flavors/rose-pine-moon.yazi/flavor.toml -> yazi_1/.config/yazi/flavors/rose-pine-moon.yazi/flavor.toml
	renamed:    yazi/.config/yazi/flavors/rose-pine-moon.yazi/preview.png -> yazi_1/.config/yazi/flavors/rose-pine-moon.yazi/preview.png
	renamed:    yazi/.config/yazi/flavors/rose-pine-moon.yazi/tmtheme.xml -> yazi_1/.config/yazi/flavors/rose-pine-moon.yazi/tmtheme.xml
	renamed:    yazi/.config/yazi/flavors/tokyo-night.yazi/LICENSE -> yazi_1/.config/yazi/flavors/tokyo-night.yazi/LICENSE
	renamed:    yazi/.config/yazi/flavors/tokyo-night.yazi/LICENSE-tmtheme -> yazi_1/.config/yazi/flavors/tokyo-night.yazi/LICENSE-tmtheme
	renamed:    yazi/.config/yazi/flavors/tokyo-night.yazi/README.md -> yazi_1/.config/yazi/flavors/tokyo-night.yazi/README.md
	renamed:    yazi/.config/yazi/flavors/tokyo-night.yazi/flavor.toml -> yazi_1/.config/yazi/flavors/tokyo-night.yazi/flavor.toml
	renamed:    yazi/.config/yazi/flavors/tokyo-night.yazi/preview.png -> yazi_1/.config/yazi/flavors/tokyo-night.yazi/preview.png
	renamed:    yazi/.config/yazi/flavors/tokyo-night.yazi/tmtheme.xml -> yazi_1/.config/yazi/flavors/tokyo-night.yazi/tmtheme.xml
	new file:   yazi_1/.config/yazi/init.lua
	new file:   yazi_1/.config/yazi/keymap.toml
	renamed:    yazi/.config/yazi/omp/omp.json -> yazi_1/.config/yazi/omp/omp.json
	renamed:    yazi/.config/yazi/omp/omp.toml -> yazi_1/.config/yazi/omp/omp.toml
	renamed:    yazi/.config/yazi/omp/omp2.toml -> yazi_1/.config/yazi/omp/omp2.toml
	renamed:    yazi/.config/yazi/omp/omp_rose-pine.toml -> yazi_1/.config/yazi/omp/omp_rose-pine.toml
	new file:   yazi_1/.config/yazi/package.toml
	renamed:    yazi/.config/yazi/plugins/bunny.yazi/LICENSE -> yazi_1/.config/yazi/plugins/bunny.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/bunny.yazi/README.md -> yazi_1/.config/yazi/plugins/bunny.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/bunny.yazi/main.lua -> yazi_1/.config/yazi/plugins/bunny.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/bypass.yazi/LICENSE -> yazi_1/.config/yazi/plugins/bypass.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/bypass.yazi/README.md -> yazi_1/.config/yazi/plugins/bypass.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/bypass.yazi/main.lua -> yazi_1/.config/yazi/plugins/bypass.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/dupes.yazi/LICENSE -> yazi_1/.config/yazi/plugins/dupes.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/dupes.yazi/README.md -> yazi_1/.config/yazi/plugins/dupes.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/dupes.yazi/main.lua -> yazi_1/.config/yazi/plugins/dupes.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/fazif.yazi/LICENSE -> yazi_1/.config/yazi/plugins/fazif.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/fazif.yazi/README.md -> yazi_1/.config/yazi/plugins/fazif.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/fazif.yazi/faziffd -> yazi_1/.config/yazi/plugins/fazif.yazi/faziffd
	renamed:    yazi/.config/yazi/plugins/fazif.yazi/faziffdr -> yazi_1/.config/yazi/plugins/fazif.yazi/faziffdr
	renamed:    yazi/.config/yazi/plugins/fazif.yazi/fazifrg -> yazi_1/.config/yazi/plugins/fazif.yazi/fazifrg
	renamed:    yazi/.config/yazi/plugins/fazif.yazi/fazifrga -> yazi_1/.config/yazi/plugins/fazif.yazi/fazifrga
	renamed:    yazi/.config/yazi/plugins/fazif.yazi/main.lua -> yazi_1/.config/yazi/plugins/fazif.yazi/main.lua
	new file:   yazi_1/.config/yazi/plugins/fchar.yazi/LICENSE
	new file:   yazi_1/.config/yazi/plugins/fchar.yazi/README.md
	new file:   yazi_1/.config/yazi/plugins/fchar.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/file-extra-metadata.yazi/LICENSE -> yazi_1/.config/yazi/plugins/file-extra-metadata.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/file-extra-metadata.yazi/README.md -> yazi_1/.config/yazi/plugins/file-extra-metadata.yazi/README.md
	new file:   yazi_1/.config/yazi/plugins/file-extra-metadata.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/git.yazi/LICENSE -> yazi_1/.config/yazi/plugins/full-border.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/full-border.yazi/README.md -> yazi_1/.config/yazi/plugins/full-border.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/full-border.yazi/main.lua -> yazi_1/.config/yazi/plugins/full-border.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/mount.yazi/LICENSE -> yazi_1/.config/yazi/plugins/git.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/git.yazi/README.md -> yazi_1/.config/yazi/plugins/git.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/git.yazi/main.lua -> yazi_1/.config/yazi/plugins/git.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/git.yazi/types.lua -> yazi_1/.config/yazi/plugins/git.yazi/types.lua
	renamed:    yazi/.config/yazi/plugins/lazygit.yazi/LICENSE -> yazi_1/.config/yazi/plugins/lazygit.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/lazygit.yazi/README.md -> yazi_1/.config/yazi/plugins/lazygit.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/lazygit.yazi/main.lua -> yazi_1/.config/yazi/plugins/lazygit.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/open-with-cmd.yazi/LICENSE -> yazi_1/.config/yazi/plugins/mediainfo.yazi/LICENSE
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/README.md
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/adobe-old.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/adobe-stable.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/assets/2025-02-15-09-14-23.png
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/assets/2025-02-15-09-15-39.png
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/assets/2025-02-15-16-51-11.png
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/assets/2025-02-15-16-52-39.png
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/audio-old.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/audio-stable.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/const.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/image-old.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/image-stable.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/main.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/none-media-preview-old.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/none-media-preview-stable.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/utils.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/video-old.lua
	new file:   yazi_1/.config/yazi/plugins/mediainfo.yazi/video-stable.lua
	renamed:    yazi/.config/yazi/plugins/piper.yazi/LICENSE -> yazi_1/.config/yazi/plugins/mime-ext.yazi/LICENSE
	new file:   yazi_1/.config/yazi/plugins/mime-ext.yazi/README.md
	new file:   yazi_1/.config/yazi/plugins/mime-ext.yazi/local.lua
	new file:   yazi_1/.config/yazi/plugins/mime-ext.yazi/main.lua
	new file:   yazi_1/.config/yazi/plugins/mime-ext.yazi/remote.lua
	renamed:    yazi/.config/yazi/plugins/rsync.yazi/LICENSE -> yazi_1/.config/yazi/plugins/mount.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/mount.yazi/README.md -> yazi_1/.config/yazi/plugins/mount.yazi/README.md
	new file:   yazi_1/.config/yazi/plugins/mount.yazi/cross.lua
	renamed:    yazi/.config/yazi/plugins/mount.yazi/main.lua -> yazi_1/.config/yazi/plugins/mount.yazi/main.lua
	new file:   yazi_1/.config/yazi/plugins/mount.yazi/sudo.lua
	renamed:    yazi/.config/yazi/plugins/omp.yazi -> yazi_1/.config/yazi/plugins/omp.yazi
	renamed:    yazi/.config/yazi/plugins/open-git-remote.yazi/LICENSE -> yazi_1/.config/yazi/plugins/open-git-remote.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/open-git-remote.yazi/README.md -> yazi_1/.config/yazi/plugins/open-git-remote.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/open-git-remote.yazi/main.lua -> yazi_1/.config/yazi/plugins/open-git-remote.yazi/main.lua
	new file:   yazi_1/.config/yazi/plugins/open-with-cmd.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/open-with-cmd.yazi/README.md -> yazi_1/.config/yazi/plugins/open-with-cmd.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/open-with-cmd.yazi/main.lua -> yazi_1/.config/yazi/plugins/open-with-cmd.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/ouch.yazi/LICENSE -> yazi_1/.config/yazi/plugins/ouch.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/ouch.yazi/README.md -> yazi_1/.config/yazi/plugins/ouch.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/ouch.yazi/main.lua -> yazi_1/.config/yazi/plugins/ouch.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/toggle-pane.yazi/LICENSE -> yazi_1/.config/yazi/plugins/piper.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/piper.yazi/README.md -> yazi_1/.config/yazi/plugins/piper.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/piper.yazi/main.lua -> yazi_1/.config/yazi/plugins/piper.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/projects.yazi/LICENSE -> yazi_1/.config/yazi/plugins/projects.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/projects.yazi/README.md -> yazi_1/.config/yazi/plugins/projects.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/projects.yazi/json.lua -> yazi_1/.config/yazi/plugins/projects.yazi/json.lua
	renamed:    yazi/.config/yazi/plugins/projects.yazi/main.lua -> yazi_1/.config/yazi/plugins/projects.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/recycle-bin.yazi/LICENSE -> yazi_1/.config/yazi/plugins/recycle-bin.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/recycle-bin.yazi/README.md -> yazi_1/.config/yazi/plugins/recycle-bin.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/recycle-bin.yazi/main.lua -> yazi_1/.config/yazi/plugins/recycle-bin.yazi/main.lua
	new file:   yazi_1/.config/yazi/plugins/rich-preview.yazi/LICENSE
	new file:   yazi_1/.config/yazi/plugins/rich-preview.yazi/README.md
	new file:   yazi_1/.config/yazi/plugins/rich-preview.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/vcs-files.yazi/LICENSE -> yazi_1/.config/yazi/plugins/rsync.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/rsync.yazi/README.md -> yazi_1/.config/yazi/plugins/rsync.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/rsync.yazi/assets/demo.gif -> yazi_1/.config/yazi/plugins/rsync.yazi/assets/demo.gif
	renamed:    yazi/.config/yazi/plugins/rsync.yazi/assets/demo.webm -> yazi_1/.config/yazi/plugins/rsync.yazi/assets/demo.webm
	renamed:    yazi/.config/yazi/plugins/rsync.yazi/main.lua -> yazi_1/.config/yazi/plugins/rsync.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/simple-tag.yazi/LICENSE -> yazi_1/.config/yazi/plugins/simple-tag.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/simple-tag.yazi/README.md -> yazi_1/.config/yazi/plugins/simple-tag.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/simple-tag.yazi/main.lua -> yazi_1/.config/yazi/plugins/simple-tag.yazi/main.lua
	new file:   yazi_1/.config/yazi/plugins/smart-enter.yazi/LICENSE
	new file:   yazi_1/.config/yazi/plugins/smart-enter.yazi/README.md
	new file:   yazi_1/.config/yazi/plugins/smart-enter.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/smart-paste.yazi/LICENSE -> yazi_1/.config/yazi/plugins/smart-paste.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/smart-paste.yazi/README.md -> yazi_1/.config/yazi/plugins/smart-paste.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/smart-paste.yazi/main.lua -> yazi_1/.config/yazi/plugins/smart-paste.yazi/main.lua
	new file:   yazi_1/.config/yazi/plugins/toggle-pane.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/toggle-pane.yazi/README.md -> yazi_1/.config/yazi/plugins/toggle-pane.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/toggle-pane.yazi/main.lua -> yazi_1/.config/yazi/plugins/toggle-pane.yazi/main.lua
	new file:   yazi_1/.config/yazi/plugins/vcs-files.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/vcs-files.yazi/README.md -> yazi_1/.config/yazi/plugins/vcs-files.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/vcs-files.yazi/main.lua -> yazi_1/.config/yazi/plugins/vcs-files.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/what-size.yazi/LICENSE -> yazi_1/.config/yazi/plugins/what-size.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/what-size.yazi/README.md -> yazi_1/.config/yazi/plugins/what-size.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/what-size.yazi/main.lua -> yazi_1/.config/yazi/plugins/what-size.yazi/main.lua
	new file:   yazi_1/.config/yazi/plugins/whoosh.yazi
	new file:   yazi_1/.config/yazi/plugins/wise-enter.yazi/LICENSE
	new file:   yazi_1/.config/yazi/plugins/wise-enter.yazi/README.md
	new file:   yazi_1/.config/yazi/plugins/wise-enter.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/yafg.yazi/LICENSE -> yazi_1/.config/yazi/plugins/yafg.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/yafg.yazi/README.md -> yazi_1/.config/yazi/plugins/yafg.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/yafg.yazi/main.lua -> yazi_1/.config/yazi/plugins/yafg.yazi/main.lua
	renamed:    yazi/.config/yazi/plugins/yaziline.yazi/LICENSE -> yazi_1/.config/yazi/plugins/yaziline.yazi/LICENSE
	renamed:    yazi/.config/yazi/plugins/yaziline.yazi/README.md -> yazi_1/.config/yazi/plugins/yaziline.yazi/README.md
	renamed:    yazi/.config/yazi/plugins/yaziline.yazi/main.lua -> yazi_1/.config/yazi/plugins/yaziline.yazi/main.lua
	renamed:    yazi/.config/yazi/tags/home/liph/.local/share/Trash/files/tags.json -> yazi_1/.config/yazi/tags/home/liph/.local/share/Trash/files/tags.json
	renamed:    yazi/.config/yazi/tags/home/liph/Downloads/tags.json -> yazi_1/.config/yazi/tags/home/liph/Downloads/tags.json
	renamed:    yazi/.config/yazi/tags/home/liph/programming/webdev/html/static/tags.json -> yazi_1/.config/yazi/tags/home/liph/programming/webdev/html/static/tags.json
	new file:   yazi_1/.config/yazi/tags/home/liph/scripts/tags.json
	renamed:    yazi/.config/yazi/tags/home/liph/tags.json -> yazi_1/.config/yazi/tags/home/liph/tags.json
	new file:   yazi_1/.config/yazi/yazi.toml
This commit is contained in:
liph committed 2026-08-29 08:59:31 +02:00
1 parent d6fd64c8af
commit a8a1b5a718
226 files changed
+17541 -3631

No files matched your search

@@ -0,0 +1,19 @@
Copyright (c) 2024 Lauri Niskanen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,135 @@
# mediainfo.yazi (fork)
<!--toc:start-->
- [mediainfo.yazi (fork)](#mediainfoyazi-fork)
- [Preview](#preview)
- [Installation](#installation)
- [Configuration:](#configuration)
- [Custom theme](#custom-theme)
<!--toc:end-->
This is a Yazi plugin for previewing media files. The preview shows thumbnail
using `ffmpeg` if available and media metadata using `mediainfo`.
> [!IMPORTANT]
> Minimum version: yazi v25.5.31.
## Preview
- Video
![video](assets/2025-02-15-09-15-39.png)
- Audio file with cover
![audio_with_cover_picture](assets/2025-02-15-09-14-23.png)
- Images
![image](assets/2025-02-15-16-52-39.png)
- Subtitle
![subrip](assets/2025-02-15-16-51-11.png)
- SVG+XML file doesn't have useful information, so it only show the image preview.
- There are more file extensions which are supported by mediainfo. Just add file's MIME type to `prepend_previewers`, `prepend_preloaders`.
Use `spotter` to determine File's MIME type. [Default is `<Tab>` key](https://github.com/sxyazi/yazi/blob/1a6abae974370702c8865459344bf256de58359e/yazi-config/preset/keymap-default.toml#L59)
## Installation
- Install mediainfo CLI:
- [https://mediaarea.net/en/MediaInfo/Download](https://mediaarea.net/en/MediaInfo/Download)
- Run this command in terminal to check if it's installed correctly:
```bash
mediainfo --version
```
If it output `Not found` then add it to your PATH environment variable. It's better to ask ChatGPT to help you (Prompt: `Add MediaInfo CLI to PATH environment variable in Windows`).
- Install ImageMagick (for linux, you can use your distro package manager to install):
https://imagemagick.org/script/download.php
- Install this plugin:
```bash
ya pkg add boydaihungst/mediainfo
```
## Configuration:
> [!IMPORTANT]
>
> `mediainfo` use built-in video, image, svg, magick plugins behind the scene to render preview image, song cover.
> So you can remove those 4 plugins from `prepend_preloaders` and `prepend_previewers` sections in `yazi.toml`.
If you have cache problem, run this cmd, and follow the tips: `yazi --clear-cache`
Config folder for each OS: https://yazi-rs.github.io/docs/configuration/overview.
Create `.../yazi/yazi.toml` and add:
> [!IMPORTANT]
>
> For yazi (>=v25.12.29) replace `name` with `url`
```toml
[plugin]
prepend_preloaders = [
# Replace magick, image, video with mediainfo
{ mime = "{audio,video,image}/*", run = "mediainfo" },
{ mime = "application/subrip", run = "mediainfo" },
# Adobe Illustrator, Adobe Photoshop is image/adobe.photoshop, already handled above
{ mime = "application/postscript", run = "mediainfo" },
]
prepend_previewers = [
# Replace magick, image, video with mediainfo
{ mime = "{audio,video,image}/*", run = "mediainfo"},
{ mime = "application/subrip", run = "mediainfo" },
# Adobe Illustrator, Adobe Photoshop is image/adobe.photoshop, already handled above
{ mime = "application/postscript", run = "mediainfo" },
]
# There are more extensions which are supported by mediainfo.
# Just add file's MIME type to `previewers`, `preloaders` above.
# https://mediaarea.net/en/MediaInfo/Support/Formats
# For a large file like Adobe Illustrator, Adobe Photoshop, etc
# you may need to increase the memory limit if no image is rendered.
# https://yazi-rs.github.io/docs/configuration/yazi#tasks
[tasks]
image_alloc = 1073741824 # = 1024*1024*1024 = 1024MB
```
## Custom theme
Using the same style with spotter. [Read more](https://github.com/sxyazi/yazi/pull/2391)
Edit or add `yazi/theme.toml`:
```toml
[spot]
# Section header style.
# Example: Video, Text, Image,... with green color in preview images above
title = { fg = "green" }
# Value style.
# Example: `Format: FLAC` with blue color in preview images above
tbl_col = { fg = "blue" }
```
## (Optional) Keymaps to hide metadata and to preview images in full screen
> [!IMPORTANT]
> Use any key you want, but make sure there is no conflicts with [default Keybindings](https://github.com/sxyazi/yazi/blob/main/yazi-config/preset/keymap-default.toml).
Since Yazi prioritizes the first matching key, `prepend_keymap` takes precedence over defaults.
Or you can use `keymap` to replace all other keys
```toml
[mgr]
prepend_keymap = [
{ on = "<F9>", run = "plugin mediainfo -- toggle-metadata", desc = "Toggle media preview metadata" },
]
```
@@ -0,0 +1,302 @@
--- @since 26.1.22
local M = {}
local const = require(".const")
local utils = require(".utils")
local function image_layer_count(job)
local cache = ya.file_cache({ file = job.file, skip = 0 })
if not cache then
return 0
end
local layer_count = utils.get_state("f" .. tostring(cache))
if layer_count then
return layer_count
end
local output, err = Command("identify")
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
if err or not output then
return 0
end
layer_count = 0
for line in output.stdout:gmatch("[^\r\n]+") do
if line:match("%S") then
layer_count = layer_count + 1
end
end
utils.set_state("f" .. tostring(cache), layer_count)
return layer_count
end
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache(job)
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local no_metadata = job.args.no_metadata
local mediainfo_job_skip = job.skip
::recalc_mediainfo_job_skip::
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not no_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not (utils.get_state(const.STATE_KEY.skip_labels) or const.skip_labels)[label] then
line = ui.Line({
ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()),
ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")),
})
end
else
if not (utils.get_state(const.STATE_KEY.skip_section_labels) or {})[str] then
line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) })
end
end
if line then
local line_height = ui.height
and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap })
or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1))
if next_str == nil and line_height == 1 then
EOF_mediainfo = true
end
if (last_line + line_height) > mediainfo_job_skip then
table.insert(lines, line)
end
if (last_line + line_height) >= mediainfo_job_skip + limit then
last_line = mediainfo_job_skip + limit
EOF_mediainfo = false
break
end
last_line = last_line + line_height
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not no_metadata then
if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then
if
image_layer_count(job)
< (
1
+ math.floor(
math.max(
0,
utils.get_state(const.STATE_KEY.units)
and (math.abs(job.skip / utils.get_state(const.STATE_KEY.units)))
or 0
)
)
)
then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
else
local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip)
mediainfo_job_skip = last_valid_mediainfo_skip
and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)]
or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0))
goto recalc_mediainfo_job_skip
end
else
utils.set_state(
const.STATE_KEY.last_valid_mediainfo_skip,
{ [tostring(cache_img_url_no_skip)] = mediainfo_job_skip }
)
end
end
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
local old_metadata_area = utils.get_state(const.STATE_KEY.prev_metadata_area)
if
old_metadata_area.win_x == job.area.x
and old_metadata_area.win_y == job.area.y
and old_metadata_area.win_w == job.area.w
and old_metadata_area.win_h == job.area.h
then
ya.preview_widget(job, {
ui.Clear(ui.Rect({
x = old_metadata_area.x,
y = old_metadata_area.y,
w = old_metadata_area.w,
h = old_metadata_area.h,
})),
})
end
end
utils.force_render()
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.STATE_KEY.prev_metadata_area, not no_metadata and {
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
win_x = job.area.x,
win_y = job.area.y,
win_w = job.area.w,
win_h = job.area.h,
} or nil)
end
function M:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Preload image
local cache_img_url = ya.file_cache(job)
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
-- NOTE: Only generate preview image when cache image is not exist
if not cache_img_url_cha or cache_img_url_cha.len <= 0 then
local cache_img_status, image_preload_err
local layer_index = 0
local units = utils.get_state(const.STATE_KEY.units)
if units ~= nil then
local max_layer = image_layer_count(job)
layer_index = math.floor(math.max(0, math.abs(job.skip / units)))
if layer_index + 1 > max_layer then
layer_index = math.max(0, max_layer - 1)
end
end
local cache_img_url_tmp = Url(cache_img_url .. ".tmp")
if fs.cha(cache_img_url_tmp) then
fs.remove("file", cache_img_url_tmp)
end
local tmp_file_path, _ = type(fs.unique) == "function" and fs.unique("file", cache_img_url_tmp)
or fs.unique_name(cache_img_url_tmp)
cache_img_status, image_preload_err = require("magick")
.with_limit()
:arg({
"-background",
"none",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) .. "[" .. tostring(
layer_index
) .. "]",
"-auto-orient",
"-strip",
"-resize",
string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height),
"-quality",
rt.preview.image_quality,
string.format("PNG32:%s", tostring(tmp_file_path)),
})
:status()
if cache_img_status then
os.rename(tostring(tmp_file_path), tostring(cache_img_url))
end
if not cache_img_status and image_preload_err then
ya.dbg("mediainfo", image_preload_err)
err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "")
end
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M
@@ -0,0 +1,338 @@
--- @since 26.5.6
local M = {}
local const = require(".const")
local utils = require(".utils")
local function image_layer_count(job)
local cache = ya.file_cache({ file = job.file, skip = 0 })
if not cache then
return 0
end
local layer_count = utils.get_state("f" .. tostring(cache))
if layer_count then
return layer_count
end
local output, err = Command("identify")
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
if err or not output then
return 0
end
layer_count = 0
for line in output.stdout:gmatch("[^\r\n]+") do
if line:match("%S") then
layer_count = layer_count + 1
end
end
utils.set_state("f" .. tostring(cache), layer_count)
return layer_count
end
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache(job)
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local no_metadata = job.args.no_metadata
local mediainfo_job_skip = job.skip
::recalc_mediainfo_job_skip::
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not no_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
local opt = { ansi = true, tab_size = rt.preview.tab_size, wrap = rt.preview.wrap, width = max_width }
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not (utils.get_state(const.STATE_KEY.skip_labels) or const.skip_labels)[label] then
line = label .. ": " .. value
end
else
if not (utils.get_state(const.STATE_KEY.skip_section_labels) or {})[str] then
line = str
end
end
if line then
local wrapped = ui.lines(line, opt)
local line_height = #wrapped
local from = 1
local to = math.min(line_height, mediainfo_job_skip + limit - last_line)
local total_rendered_text_len = 1
local total_label_rendered_len = 1
local label_total_len = label and utf8.len(label .. ": ") or 0
for j = from, to do
local current_line_components = {}
local wrapped_line_len = wrapped[j]:width() or 0
local wrapped_raw =
utils.utf8_sub(line, total_rendered_text_len, total_rendered_text_len + wrapped_line_len)
wrapped_line_len = utf8.len(wrapped_raw)
total_rendered_text_len = total_rendered_text_len + wrapped_line_len
if last_line + 1 > mediainfo_job_skip then
local label_raw = label_total_len - total_label_rendered_len <= 0 and ""
or utils.utf8_sub(wrapped_raw, 1, label_total_len - total_label_rendered_len)
local label_raw_len = utf8.len(label_raw)
if label_raw_len > 0 then
table.insert(
current_line_components,
ui.Span(label_raw):style(ui.Style():fg("reset"):bold())
)
total_label_rendered_len = total_label_rendered_len + label_raw_len
wrapped_raw = wrapped_raw:gsub("^" .. utils.is_literal_string(label_raw), "", 1)
end
if total_label_rendered_len >= label_total_len then
local value_raw = wrapped_raw
table.insert(
current_line_components,
ui.Span(value_raw or ""):style(
label and (th.spot.tbl_col or ui.Style():fg("blue"))
or (th.spot.title or ui.Style():fg("green"))
)
)
end
table.insert(lines, ui.Line(current_line_components))
-- last_line = last_line + 1
else
total_label_rendered_len = total_label_rendered_len + total_rendered_text_len
end
last_line = last_line + 1
if next_str == nil and not wrapped[j + 1] then
EOF_mediainfo = true
end
if last_line >= mediainfo_job_skip + limit then
last_line = mediainfo_job_skip + limit
EOF_mediainfo = false
break
end
end
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not no_metadata then
if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then
if
image_layer_count(job)
< (
1
+ math.floor(
math.max(
0,
utils.get_state(const.STATE_KEY.units)
and (math.abs(job.skip / utils.get_state(const.STATE_KEY.units)))
or 0
)
)
)
then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
else
local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip)
mediainfo_job_skip = last_valid_mediainfo_skip
and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)]
or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0))
goto recalc_mediainfo_job_skip
end
else
utils.set_state(
const.STATE_KEY.last_valid_mediainfo_skip,
{ [tostring(cache_img_url_no_skip)] = mediainfo_job_skip }
)
end
end
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
local old_metadata_area = utils.get_state(const.STATE_KEY.prev_metadata_area)
if
old_metadata_area.win_x == job.area.x
and old_metadata_area.win_y == job.area.y
and old_metadata_area.win_w == job.area.w
and old_metadata_area.win_h == job.area.h
then
ya.preview_widget(job, {
ui.Clear(ui.Rect({
x = old_metadata_area.x,
y = old_metadata_area.y,
w = old_metadata_area.w,
h = old_metadata_area.h,
})),
})
end
end
utils.force_render()
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.STATE_KEY.prev_metadata_area, not no_metadata and {
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
win_x = job.area.x,
win_y = job.area.y,
win_w = job.area.w,
win_h = job.area.h,
} or nil)
end
function M:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Preload image
local cache_img_url = ya.file_cache(job)
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
-- NOTE: Only generate preview image when cache image is not exist
if not cache_img_url_cha or cache_img_url_cha.len <= 0 then
local cache_img_status, image_preload_err
local layer_index = 0
local units = utils.get_state(const.STATE_KEY.units)
if units ~= nil then
local max_layer = image_layer_count(job)
layer_index = math.floor(math.max(0, math.abs(job.skip / units)))
if layer_index + 1 > max_layer then
layer_index = math.max(0, max_layer - 1)
end
end
local cache_img_url_tmp = Url(cache_img_url .. ".tmp")
if fs.cha(cache_img_url_tmp) then
fs.remove("file", cache_img_url_tmp)
end
local tmp_file_path, _ = type(fs.unique) == "function" and fs.unique("file", cache_img_url_tmp)
or fs.unique_name(cache_img_url_tmp)
cache_img_status, image_preload_err = require("magick")
.with_limit()
:arg({
"-background",
"none",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) .. "[" .. tostring(
layer_index
) .. "]",
"-auto-orient",
"-strip",
"-resize",
string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height),
"-quality",
rt.preview.image_quality,
string.format("PNG32:%s", tostring(tmp_file_path)),
})
:status()
if cache_img_status then
os.rename(tostring(tmp_file_path), tostring(cache_img_url))
end
if not cache_img_status and image_preload_err then
ya.dbg("mediainfo", image_preload_err)
err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "")
end
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

@@ -0,0 +1,345 @@
--- @since 26.1.22
local M = {}
local const = require(".const")
local utils = require(".utils")
local function get_cover_layers(job)
local cache = ya.file_cache({ file = job.file, skip = 0 })
if not cache then
return {}
end
local covers = utils.get_state("f" .. tostring(cache))
if covers and type(covers) == "table" then
return covers
end
local output, err = Command("ffprobe"):arg({
"-v",
"error",
"-select_streams",
"v",
"-show_entries",
"stream=index:stream_disposition=attached_pic",
"-of",
"json",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
}):output()
if err or not output then
return {}
end
covers = {}
local data = ya.json_decode(output.stdout)
if type(data.streams) == "table" then
for _, stream in ipairs(data.streams) do
if stream.disposition and stream.disposition.attached_pic then
covers[#covers + 1] = stream.index
end
end
end
utils.set_state("f" .. tostring(cache), covers)
return covers
end
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache(job)
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local no_metadata = job.args.no_metadata
local mediainfo_job_skip = job.skip
::recalc_mediainfo_job_skip::
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not no_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not (utils.get_state(const.STATE_KEY.skip_labels) or const.skip_labels)[label] then
line = ui.Line({
ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()),
ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")),
})
end
else
if not (utils.get_state(const.STATE_KEY.skip_section_labels) or {})[str] then
line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) })
end
end
if line then
local line_height = ui.height
and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap })
or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1))
if next_str == nil and line_height == 1 then
EOF_mediainfo = true
end
if (last_line + line_height) > mediainfo_job_skip then
table.insert(lines, line)
end
if (last_line + line_height) >= mediainfo_job_skip + limit then
last_line = mediainfo_job_skip + limit
EOF_mediainfo = false
break
end
last_line = last_line + line_height
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not no_metadata then
if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then
local covers = get_cover_layers(job)
local cover_index = math.floor(
math.max(
1,
utils.get_state(const.STATE_KEY.units)
and (math.abs(job.skip / utils.get_state(const.STATE_KEY.units)))
or 0
)
)
if not covers[cover_index] then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
else
-- NOTE: Recalculate mediainfo using cached latest valid skip value when reach the end of mediainfo output
local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip)
mediainfo_job_skip = last_valid_mediainfo_skip
and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)]
or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0))
goto recalc_mediainfo_job_skip
end
else
utils.set_state(
const.STATE_KEY.last_valid_mediainfo_skip,
{ [tostring(cache_img_url_no_skip)] = mediainfo_job_skip }
)
end
end
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
local old_metadata_area = utils.get_state(const.STATE_KEY.prev_metadata_area)
if
old_metadata_area.win_x == job.area.x
and old_metadata_area.win_y == job.area.y
and old_metadata_area.win_w == job.area.w
and old_metadata_area.win_h == job.area.h
then
ya.preview_widget(job, {
ui.Clear(ui.Rect({
x = old_metadata_area.x,
y = old_metadata_area.y,
w = old_metadata_area.w,
h = old_metadata_area.h,
})),
})
end
end
utils.force_render()
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.STATE_KEY.prev_metadata_area, not no_metadata and {
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
win_x = job.area.x,
win_y = job.area.y,
win_w = job.area.w,
win_h = job.area.h,
} or nil)
end
function M:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Preload image
local cache_img_url = ya.file_cache(job)
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
-- NOTE: Only generate preview image when cache image is not exist
if cache_img_url and (not cache_img_url_cha or cache_img_url_cha.len <= 0) then
local units = utils.get_state(const.STATE_KEY.units)
local covers = get_cover_layers(job)
local cover_index = covers[1] or 0
if units ~= nil then
cover_index = math.floor(math.max(1, math.abs(job.skip / units)))
if not covers[cover_index] then
cover_index = math.max(0, #covers - 1)
end
end
local qv = 31 - math.floor(rt.preview.image_quality * 0.3)
local audio_preload_output, audio_preload_err = Command("ffmpeg"):arg({
"-v",
"error",
"-threads",
1,
"-i",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-map",
string.format("0:v:%d?", cover_index),
"-an",
"-sn",
"-dn",
"-vframes",
1,
"-q:v",
qv,
"-vf",
string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height / 2),
"-f",
"image2",
"-y",
tostring(cache_img_url),
}):output()
-- NOTE: Some audio types doesn't have cover image -> error ""
if audio_preload_err then
ya.dbg("mediainfo", audio_preload_err)
err_msg = err_msg
.. string.format(
"Failed to start `%s`.\n Error: %s\n",
"ffmpeg",
tostring(audio_preload_err or (audio_preload_output and audio_preload_output.stderr or ""))
)
elseif
audio_preload_output
and type(audio_preload_output.stderr) == "string"
and audio_preload_output.stderr:find("does not contain any stream")
then
ya.dbg("mediainfo", audio_preload_output and audio_preload_output.stderr)
cache_img_url_cha, _ = fs.cha(cache_img_url)
if cache_img_url_cha then
fs.remove("file", Url(cache_img_url))
end
-- NOTE: Workaround case audio has no cover image. Prevent regenerate preview image
audio_preload_output, audio_preload_err = require("magick")
.with_limit()
:arg({
"-size",
"1x1",
"canvas:none",
string.format("PNG32:%s", cache_img_url),
})
:output()
if
(audio_preload_output and audio_preload_output.stderr ~= nil and audio_preload_output.stderr ~= "")
or audio_preload_err
then
ya.dbg("mediainfo", audio_preload_err or (audio_preload_output and audio_preload_output.stderr))
err_msg = err_msg
.. string.format(
"Failed to start `%s`.\n Error: %s\n",
"magick",
tostring(audio_preload_err or (audio_preload_output and audio_preload_output.stderr or ""))
)
end
end
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M
@@ -0,0 +1,381 @@
--- @since 26.5.6
local M = {}
local const = require(".const")
local utils = require(".utils")
local function get_cover_layers(job)
local cache = ya.file_cache({ file = job.file, skip = 0 })
if not cache then
return {}
end
local covers = utils.get_state("f" .. tostring(cache))
if covers and type(covers) == "table" then
return covers
end
local output, err = Command("ffprobe"):arg({
"-v",
"error",
"-select_streams",
"v",
"-show_entries",
"stream=index:stream_disposition=attached_pic",
"-of",
"json",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
}):output()
if err or not output then
return {}
end
covers = {}
local data = ya.json_decode(output.stdout)
if type(data.streams) == "table" then
for _, stream in ipairs(data.streams) do
if stream.disposition and stream.disposition.attached_pic then
covers[#covers + 1] = stream.index
end
end
end
utils.set_state("f" .. tostring(cache), covers)
return covers
end
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache(job)
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local no_metadata = job.args.no_metadata
local mediainfo_job_skip = job.skip
::recalc_mediainfo_job_skip::
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not no_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
local opt = { ansi = true, tab_size = rt.preview.tab_size, wrap = rt.preview.wrap, width = max_width }
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not (utils.get_state(const.STATE_KEY.skip_labels) or const.skip_labels)[label] then
line = label .. ": " .. value
end
else
if not (utils.get_state(const.STATE_KEY.skip_section_labels) or {})[str] then
line = str
end
end
if line then
local wrapped = ui.lines(line, opt)
local line_height = #wrapped
local from = 1
local to = math.min(line_height, mediainfo_job_skip + limit - last_line)
local total_rendered_text_len = 1
local total_label_rendered_len = 1
local label_total_len = label and utf8.len(label .. ": ") or 0
for j = from, to do
local current_line_components = {}
local wrapped_line_len = wrapped[j]:width() or 0
local wrapped_raw =
utils.utf8_sub(line, total_rendered_text_len, total_rendered_text_len + wrapped_line_len)
wrapped_line_len = utf8.len(wrapped_raw)
total_rendered_text_len = total_rendered_text_len + wrapped_line_len
if last_line + 1 > mediainfo_job_skip then
local label_raw = label_total_len - total_label_rendered_len <= 0 and ""
or utils.utf8_sub(wrapped_raw, 1, label_total_len - total_label_rendered_len)
local label_raw_len = utf8.len(label_raw)
if label_raw_len > 0 then
table.insert(
current_line_components,
ui.Span(label_raw):style(ui.Style():fg("reset"):bold())
)
total_label_rendered_len = total_label_rendered_len + label_raw_len
wrapped_raw = wrapped_raw:gsub("^" .. utils.is_literal_string(label_raw), "", 1)
end
if total_label_rendered_len >= label_total_len then
local value_raw = wrapped_raw
table.insert(
current_line_components,
ui.Span(value_raw or ""):style(
label and (th.spot.tbl_col or ui.Style():fg("blue"))
or (th.spot.title or ui.Style():fg("green"))
)
)
end
table.insert(lines, ui.Line(current_line_components))
-- last_line = last_line + 1
else
total_label_rendered_len = total_label_rendered_len + total_rendered_text_len
end
last_line = last_line + 1
if next_str == nil and not wrapped[j + 1] then
EOF_mediainfo = true
end
if last_line >= mediainfo_job_skip + limit then
last_line = mediainfo_job_skip + limit
EOF_mediainfo = false
break
end
end
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not no_metadata then
if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then
local covers = get_cover_layers(job)
local cover_index = math.floor(
math.max(
1,
utils.get_state(const.STATE_KEY.units)
and (math.abs(job.skip / utils.get_state(const.STATE_KEY.units)))
or 0
)
)
if not covers[cover_index] then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
else
-- NOTE: Recalculate mediainfo using cached latest valid skip value when reach the end of mediainfo output
local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip)
mediainfo_job_skip = last_valid_mediainfo_skip
and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)]
or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0))
goto recalc_mediainfo_job_skip
end
else
utils.set_state(
const.STATE_KEY.last_valid_mediainfo_skip,
{ [tostring(cache_img_url_no_skip)] = mediainfo_job_skip }
)
end
end
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
local old_metadata_area = utils.get_state(const.STATE_KEY.prev_metadata_area)
if
old_metadata_area.win_x == job.area.x
and old_metadata_area.win_y == job.area.y
and old_metadata_area.win_w == job.area.w
and old_metadata_area.win_h == job.area.h
then
ya.preview_widget(job, {
ui.Clear(ui.Rect({
x = old_metadata_area.x,
y = old_metadata_area.y,
w = old_metadata_area.w,
h = old_metadata_area.h,
})),
})
end
end
utils.force_render()
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.STATE_KEY.prev_metadata_area, not no_metadata and {
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
win_x = job.area.x,
win_y = job.area.y,
win_w = job.area.w,
win_h = job.area.h,
} or nil)
end
function M:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Preload image
local cache_img_url = ya.file_cache(job)
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
-- NOTE: Only generate preview image when cache image is not exist
if cache_img_url and (not cache_img_url_cha or cache_img_url_cha.len <= 0) then
local units = utils.get_state(const.STATE_KEY.units)
local covers = get_cover_layers(job)
local cover_index = covers[1] or 0
if units ~= nil then
cover_index = math.floor(math.max(1, math.abs(job.skip / units)))
if not covers[cover_index] then
cover_index = math.max(0, #covers - 1)
end
end
local qv = 31 - math.floor(rt.preview.image_quality * 0.3)
local audio_preload_output, audio_preload_err = Command("ffmpeg"):arg({
"-v",
"error",
"-threads",
1,
"-i",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-map",
string.format("0:v:%d?", cover_index),
"-an",
"-sn",
"-dn",
"-vframes",
1,
"-q:v",
qv,
"-vf",
string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height / 2),
"-f",
"image2",
"-y",
tostring(cache_img_url),
}):output()
-- NOTE: Some audio types doesn't have cover image -> error ""
if audio_preload_err then
ya.dbg("mediainfo", audio_preload_err)
err_msg = err_msg
.. string.format(
"Failed to start `%s`.\n Error: %s\n",
"ffmpeg",
tostring(audio_preload_err or (audio_preload_output and audio_preload_output.stderr or ""))
)
elseif
audio_preload_output
and type(audio_preload_output.stderr) == "string"
and audio_preload_output.stderr:find("does not contain any stream")
then
ya.dbg("mediainfo", audio_preload_output and audio_preload_output.stderr)
cache_img_url_cha, _ = fs.cha(cache_img_url)
if cache_img_url_cha then
fs.remove("file", Url(cache_img_url))
end
-- NOTE: Workaround case audio has no cover image. Prevent regenerate preview image
audio_preload_output, audio_preload_err = require("magick")
.with_limit()
:arg({
"-size",
"1x1",
"canvas:none",
string.format("PNG32:%s", cache_img_url),
})
:output()
if
(audio_preload_output and audio_preload_output.stderr ~= nil and audio_preload_output.stderr ~= "")
or audio_preload_err
then
ya.dbg("mediainfo", audio_preload_err or (audio_preload_output and audio_preload_output.stderr))
err_msg = err_msg
.. string.format(
"Failed to start `%s`.\n Error: %s\n",
"magick",
tostring(audio_preload_err or (audio_preload_output and audio_preload_output.stderr or ""))
)
end
end
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M
@@ -0,0 +1,68 @@
--- @since 26.1.22
local M = {}
M.skip_labels = {
["Complete name"] = true,
["CompleteName_Last"] = true,
["Unique ID"] = true,
["File size"] = true,
["Format/Info"] = true,
["Codec ID/Info"] = true,
["MD5 of the unencoded content"] = true,
}
M.ENTRY_ACTION = {
toggle_metadata = "toggle-metadata",
toggle_preview = "toggle-preview",
hide_metadata = "hide-metadata",
hide_preview = "hide-preview",
show_metadata = "show-metadata",
show_preview = "show-preview",
reset = "reset",
}
M.STATE_KEY = {
skip_labels = "skip_labels",
skip_section_labels = "skip_section_labels",
units = "units",
no_metadata = "no_metadata",
no_preview = "no_preview",
prev_metadata_area = "prev_metadata_area",
prev_image_height = "prev_image_height",
last_valid_mediainfo_skip = "last_valid_mediainfo_skip",
cached_mediainfo = "cached_mediainfo",
cached_job_args = "cached_job_args",
}
M.magick_image_mimes = {
avif = true,
hei = true,
heic = true,
heif = true,
["heif-sequence"] = true,
["heic-sequence"] = true,
jxl = true,
tiff = true,
xml = true,
-- ["svg+xml"] = true,
["canon-cr2"] = true,
}
M.seekable_mimes = {
-- NOTE: Adobe illustrator photoshop mimetypes
["application/postscript"] = true,
["application/dvb.ait"] = true,
["application/illustrator"] = true,
["application/vnd.adobe.illustrator"] = true,
["image/x-eps"] = true,
["application/eps"] = true,
["application/pdf"] = true,
["image/adobe.photoshop"] = true,
}
M.suffix = "_mediainfo"
M.SHELL = os.getenv("SHELL") or ""
return M
@@ -0,0 +1,253 @@
--- @since 26.1.22
local M = {}
local const = require(".const")
local utils = require(".utils")
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache({
skip = 0,
args = job.args,
file = job.file,
area = job.area,
})
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local no_metadata = job.args.no_metadata
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not no_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not (utils.get_state(const.STATE_KEY.skip_labels) or const.skip_labels)[label] then
line = ui.Line({
ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()),
ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")),
})
end
else
if not (utils.get_state(const.STATE_KEY.skip_section_labels) or {})[str] then
line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) })
end
end
if line then
local line_height = ui.height
and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap })
or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1))
if next_str == nil and line_height == 1 then
EOF_mediainfo = true
end
if (last_line + line_height) > job.skip then
table.insert(lines, line)
end
if (last_line + line_height) >= job.skip + limit then
last_line = job.skip + limit
EOF_mediainfo = false
break
end
last_line = last_line + line_height
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not no_metadata and EOF_mediainfo and #lines == 0 and job.skip > 0 then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
end
utils.force_render()
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
local old_metadata_area = utils.get_state(const.STATE_KEY.prev_metadata_area)
if
old_metadata_area.win_x == job.area.x
and old_metadata_area.win_y == job.area.y
and old_metadata_area.win_w == job.area.w
and old_metadata_area.win_h == job.area.h
then
ya.preview_widget(job, {
ui.Clear(ui.Rect({
x = old_metadata_area.x,
y = old_metadata_area.y,
w = old_metadata_area.w,
h = old_metadata_area.h,
})),
})
end
end
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.STATE_KEY.prev_metadata_area, not no_metadata and {
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
win_x = job.area.x,
win_y = job.area.y,
win_w = job.area.w,
win_h = job.area.h,
} or nil)
end
function M:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Preload image
local mime = job.mime:match(".*/(.*)$")
local is_svg = mime == "svg+xml"
local is_magick = const.magick_image_mimes[mime]
local no_skip_job = { skip = 0, file = job.file, args = job.args, area = job.area }
local cache_img_url = ya.file_cache(no_skip_job)
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
-- NOTE: Only generate preview image when cache image is not exist
if not cache_img_url_cha or cache_img_url_cha.len <= 0 then
local cache_img_status, image_preload_err
if not is_valid_utf8_path then
-- NOTE: Case not valid utf8 path, use trick to generate preview image
if is_svg then
local cache_img_url_tmp = Url(cache_img_url .. ".tmp")
if fs.cha(cache_img_url_tmp) then
fs.remove("file", cache_img_url_tmp)
end
local tmp_file_path, _ = type(fs.unique) == "function" and fs.unique("file", cache_img_url_tmp)
or fs.unique_name(cache_img_url_tmp)
-- svg under invalid utf8 path
cache_img_status, image_preload_err = require("magick")
.with_limit()
:arg({
"-background",
"none",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-auto-orient",
"-strip",
string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height),
"-quality",
rt.preview.image_quality,
string.format("PNG32:%s", tostring(tmp_file_path)),
})
:status()
if cache_img_status then
os.rename(tostring(tmp_file_path), tostring(cache_img_url))
end
end
else
-- NOTE: Case valid utf8 path, use image, svg, or magick module
local image_module = is_svg and "svg" or (is_magick and "magick" or "image")
cache_img_status, image_preload_err = require(image_module):preload(no_skip_job)
end
if not cache_img_status and image_preload_err then
ya.dbg("mediainfo", image_preload_err)
err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "")
end
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M
@@ -0,0 +1,290 @@
--- @since 26.5.6
local M = {}
local const = require(".const")
local utils = require(".utils")
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache({
skip = 0,
args = job.args,
file = job.file,
area = job.area,
})
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local no_metadata = job.args.no_metadata
local mediainfo_job_skip = job.skip
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not no_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
local opt = { ansi = true, tab_size = rt.preview.tab_size, wrap = rt.preview.wrap, width = max_width }
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not (utils.get_state(const.STATE_KEY.skip_labels) or const.skip_labels)[label] then
line = label .. ": " .. value
end
else
if not (utils.get_state(const.STATE_KEY.skip_section_labels) or {})[str] then
line = str
end
end
if line then
local wrapped = ui.lines(line, opt)
local line_height = #wrapped
local from = 1
local to = math.min(line_height, mediainfo_job_skip + limit - last_line)
local total_rendered_text_len = 1
local total_label_rendered_len = 1
local label_total_len = label and utf8.len(label .. ": ") or 0
for j = from, to do
local current_line_components = {}
local wrapped_line_len = wrapped[j]:width() or 0
local wrapped_raw =
utils.utf8_sub(line, total_rendered_text_len, total_rendered_text_len + wrapped_line_len)
wrapped_line_len = utf8.len(wrapped_raw)
total_rendered_text_len = total_rendered_text_len + wrapped_line_len
if last_line + 1 > mediainfo_job_skip then
local label_raw = label_total_len - total_label_rendered_len <= 0 and ""
or utils.utf8_sub(wrapped_raw, 1, label_total_len - total_label_rendered_len)
local label_raw_len = utf8.len(label_raw)
if label_raw_len > 0 then
table.insert(
current_line_components,
ui.Span(label_raw):style(ui.Style():fg("reset"):bold())
)
total_label_rendered_len = total_label_rendered_len + label_raw_len
wrapped_raw = wrapped_raw:gsub("^" .. utils.is_literal_string(label_raw), "", 1)
end
if total_label_rendered_len >= label_total_len then
local value_raw = wrapped_raw
table.insert(
current_line_components,
ui.Span(value_raw or ""):style(
label and (th.spot.tbl_col or ui.Style():fg("blue"))
or (th.spot.title or ui.Style():fg("green"))
)
)
end
table.insert(lines, ui.Line(current_line_components))
-- last_line = last_line + 1
else
total_label_rendered_len = total_label_rendered_len + total_rendered_text_len
end
last_line = last_line + 1
if next_str == nil and not wrapped[j + 1] then
EOF_mediainfo = true
end
if last_line >= mediainfo_job_skip + limit then
last_line = mediainfo_job_skip + limit
EOF_mediainfo = false
break
end
end
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not no_metadata and EOF_mediainfo and #lines == 0 and job.skip > 0 then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
end
utils.force_render()
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
local old_metadata_area = utils.get_state(const.STATE_KEY.prev_metadata_area)
if
old_metadata_area.win_x == job.area.x
and old_metadata_area.win_y == job.area.y
and old_metadata_area.win_w == job.area.w
and old_metadata_area.win_h == job.area.h
then
ya.preview_widget(job, {
ui.Clear(ui.Rect({
x = old_metadata_area.x,
y = old_metadata_area.y,
w = old_metadata_area.w,
h = old_metadata_area.h,
})),
})
end
end
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.STATE_KEY.prev_metadata_area, not no_metadata and {
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
win_x = job.area.x,
win_y = job.area.y,
win_w = job.area.w,
win_h = job.area.h,
} or nil)
end
function M:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Preload image
local mime = job.mime:match(".*/(.*)$")
local is_svg = mime == "svg+xml"
local is_magick = const.magick_image_mimes[mime]
local no_skip_job = { skip = 0, file = job.file, args = job.args, area = job.area }
local cache_img_url = ya.file_cache(no_skip_job)
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
-- NOTE: Only generate preview image when cache image is not exist
if not cache_img_url_cha or cache_img_url_cha.len <= 0 then
local cache_img_status, image_preload_err
if not is_valid_utf8_path then
-- NOTE: Case not valid utf8 path, use trick to generate preview image
if is_svg then
local cache_img_url_tmp = Url(cache_img_url .. ".tmp")
if fs.cha(cache_img_url_tmp) then
fs.remove("file", cache_img_url_tmp)
end
local tmp_file_path, _ = type(fs.unique) == "function" and fs.unique("file", cache_img_url_tmp)
or fs.unique_name(cache_img_url_tmp)
-- svg under invalid utf8 path
cache_img_status, image_preload_err = require("magick")
.with_limit()
:arg({
"-background",
"none",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-auto-orient",
"-strip",
string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height),
"-quality",
rt.preview.image_quality,
string.format("PNG32:%s", tostring(tmp_file_path)),
})
:status()
if cache_img_status then
os.rename(tostring(tmp_file_path), tostring(cache_img_url))
end
end
else
-- NOTE: Case valid utf8 path, use image, svg, or magick module
local image_module = is_svg and "svg" or (is_magick and "magick" or "image")
cache_img_status, image_preload_err = require(image_module):preload(no_skip_job)
end
if not cache_img_status and image_preload_err then
ya.dbg("mediainfo", image_preload_err)
err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "")
end
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M
@@ -0,0 +1,498 @@
--- @since 25.5.31
local skip_labels = {
["Complete name"] = true,
["CompleteName_Last"] = true,
["Unique ID"] = true,
["File size"] = true,
["Format/Info"] = true,
["Codec ID/Info"] = true,
["MD5 of the unencoded content"] = true,
}
local ENTRY_ACTION = {
toggle_metadata = "toggle-metadata",
}
local STATE_KEY = {
units = "units",
hide_metadata = "hide_metadata",
prev_metadata_area = "prev_metadata_area",
prev_peek_data = "prev_peek_data",
}
local magick_image_mimes = {
avif = true,
hei = true,
heic = true,
heif = true,
["heif-sequence"] = true,
["heic-sequence"] = true,
jxl = true,
xml = true,
["svg+xml"] = true,
["canon-cr2"] = true,
}
local seekable_mimes = {
["application/postscript"] = true,
["image/adobe.photoshop"] = true,
}
local M = {}
local suffix = "_mediainfo"
local SHELL = os.getenv("SHELL") or ""
local function is_valid_utf8(str)
return utf8.len(str) ~= nil
end
local function path_quote(path)
if not path or tostring(path) == "" then
return path
end
local result = "'" .. string.gsub(tostring(path), "'", "'\\''") .. "'"
return result
end
local function read_mediainfo_cached_file(file_path)
-- Open the file in read mode
local file = io.open(file_path, "r")
if file then
-- Read the entire file content
local content = file:read("*all")
file:close()
return content
end
end
local set_state = ya.sync(function(state, key, value)
state[key] = value
end)
local get_state = ya.sync(function(state, key)
return state[key]
end)
local force_render = ya.sync(function(_, _)
(ui.render or ya.render)()
end)
local function image_layer_count(job)
local cache = ya.file_cache({ file = job.file, skip = 0 })
if not cache then
return 0
end
local layer_count = get_state("f" .. tostring(cache))
if layer_count then
return layer_count
end
local output, err = Command("identify")
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
if err then
return 0
end
layer_count = 0
for line in output.stdout:gmatch("[^\r\n]+") do
if line:match("%S") then
layer_count = layer_count + 1
end
end
set_state("f" .. tostring(cache), layer_count)
return layer_count
end
function M:peek(job)
local start = os.clock()
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
if not job.mime then
return
end
set_state(STATE_KEY.prev_peek_data, {
file = tostring(job.file.path or job.file.cache or job.file.url),
mime = job.mime,
area = {
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = job.area.h,
},
args = job.args,
skip = job.skip,
})
local is_video = string.find(job.mime, "^video/")
local is_audio = string.find(job.mime, "^audio/")
local is_image = string.find(job.mime, "^image/")
local is_seekable = seekable_mimes[job.mime] or is_video
local cache_img_url = (is_audio or is_image) and cache_img_url_no_skip
if is_seekable then
cache_img_url = ya.file_cache(job)
end
local preload_status, preload_err = self:preload(job)
if not preload_status then
return
end
ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock()))
local hide_metadata = get_state(STATE_KEY.hide_metadata)
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local is_wrap = rt.preview.wrap == "yes"
if not hide_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. suffix
local output = read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = read_mediainfo_cached_file(cache_mediainfo_path)
end
for str in output:gsub("\n+$", ""):gmatch("[^\n]*") do
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not skip_labels[label] then
line = ui.Line({
ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()),
ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")),
})
end
elseif str ~= "General" then
line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) })
end
if line then
local line_height = math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1)
if (last_line + line_height) > job.skip then
table.insert(lines, line)
end
if (last_line + line_height) >= job.skip + limit then
last_line = job.skip + limit
break
end
last_line = last_line + line_height
end
end
end
mediainfo_height = math.min(limit, last_line)
end
if
(job.skip > 0 and #lines == 0 and not hide_metadata)
and (
not is_seekable
or (is_video and job.skip >= 90)
or (
(job.mime == "image/adobe.photoshop" or job.mime == "application/postscript")
and image_layer_count(job)
< (1 + math.floor(
math.max(0, get_state(STATE_KEY.units) and (job.skip / get_state(STATE_KEY.units)) or 0)
))
)
)
then
ya.emit("peek", {
math.max(0, job.skip - (get_state(STATE_KEY.units) or limit)),
only_if = job.file.url,
upper_bound = true,
})
return
end
force_render()
-- NOTE: Hacky way to prevent image overlap with old metadata area
if hide_metadata and get_state(STATE_KEY.prev_metadata_area) then
ya.preview_widget(job, {
ui.Clear(ui.Rect(get_state(STATE_KEY.prev_metadata_area))),
})
end
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- NOTE: Workaround case audio has no cover image. Prevent regenerate preview image
if is_audio and image_height == 1 then
local info = ya.image_info(cache_img_url)
if not info or (info.w == 1 and info.h == 1) then
image_height = 0
end
end
-- NOTE: Workaround case video.lua doesn't doesn't generate preview image because of `skip` overflow video duration
if is_video and not rendered_img_rect then
image_height = math.max(job.area.h - mediainfo_height, 0)
end
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
if not hide_metadata then
set_state(STATE_KEY.prev_metadata_area, {
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
})
end
end
function M:seek(job)
local h = cx.active.current.hovered
if h and h.url == job.file.url then
set_state(STATE_KEY.units, job.units)
ya.emit("peek", {
math.max(0, cx.active.preview.skip + job.units),
only_if = job.file.url,
})
end
end
function M:re_peek()
local prev_peek_data = get_state(STATE_KEY.prev_peek_data)
if prev_peek_data then
prev_peek_data.file = File({
url = Url(prev_peek_data.file),
cha = fs.cha(Url(prev_peek_data.file)),
})
prev_peek_data.area = ui.area and ui.area("preview") or ui.Rect(prev_peek_data.area)
self:peek(prev_peek_data)
end
end
function M:preload(job)
local cache_img_url = ya.file_cache({ file = job.file, skip = 0 })
if not cache_img_url then
return true
end
local cache_mediainfo_url = Url(tostring(cache_img_url) .. suffix)
cache_img_url = seekable_mimes[job.mime] and ya.file_cache(job) or cache_img_url
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
local err_msg = ""
local is_valid_utf8_path = is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- video mimetype
if job.mime then
if string.find(job.mime, "^video/") then
local cache_img_status, video_preload_err = require("video"):preload(job)
if not cache_img_status and video_preload_err then
err_msg = err_msg
.. string.format("Failed to start `%s`, Do you have `%s` installed?\n", "ffmpeg", "ffmpeg")
end
-- audo and image mimetype
elseif cache_img_url and (not cache_img_url_cha or cache_img_url_cha.len <= 0) then
-- audio
if string.find(job.mime, "^audio/") then
local qv = 31 - math.floor(rt.preview.image_quality * 0.3)
local audio_preload_output, audio_preload_err = Command("ffmpeg"):arg({
"-v",
"error",
"-threads",
1,
"-skip_frame",
"nokey",
"-an",
"-sn",
"-dn",
"-i",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-vframes",
1,
"-q:v",
qv,
"-vf",
string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height / 2),
"-f",
"image2",
"-y",
tostring(cache_img_url),
}):output()
-- NOTE: Some audio types doesn't have cover image -> error ""
if
(audio_preload_output and audio_preload_output.stderr ~= nil and audio_preload_output.stderr ~= "")
or audio_preload_err
then
err_msg = err_msg
.. string.format("Failed to start `%s`, Do you have `%s` installed?\n", "ffmpeg", "ffmpeg")
else
cache_img_url_cha, _ = fs.cha(cache_img_url)
if not cache_img_url_cha then
-- NOTE: Workaround case audio has no cover image. Prevent regenerate preview image
audio_preload_output, audio_preload_err = require("magick")
.with_limit()
:arg({
"-size",
"1x1",
"canvas:none",
string.format("PNG32:%s", cache_img_url),
})
:output()
if
(audio_preload_output.stderr ~= nil and audio_preload_output.stderr ~= "")
or audio_preload_err
then
err_msg = err_msg
.. string.format(
"Failed to start `%s`, Do you have `%s` installed?\n",
"magick",
"magick"
)
end
end
end
-- image
elseif string.find(job.mime, "^image/") or job.mime == "application/postscript" then
local svg_plugin_ok, svg_plugin = pcall(require, "svg")
local _, magick_plugin = pcall(require, "magick")
local mime = job.mime:match(".*/(.*)$")
local image_plugin = magick_image_mimes[mime]
and ((mime == "svg+xml" and svg_plugin_ok) and svg_plugin or magick_plugin)
or require("image")
local cache_img_status, image_preload_err
-- psd, ai, eps
if mime == "adobe.photoshop" or job.mime == "application/postscript" then
local layer_index = 0
local units = get_state(STATE_KEY.units)
if units ~= nil then
local max_layer = image_layer_count(job)
layer_index = math.floor(math.max(0, job.skip / units))
if layer_index + 1 > max_layer then
layer_index = max_layer - 1
end
end
local cache_img_url_tmp = Url(cache_img_url .. ".tmp")
if fs.cha(cache_img_url_tmp) then
fs.remove("file", cache_img_url_tmp)
end
local tmp_file_path, _ = fs.unique_name(cache_img_url_tmp)
cache_img_status, image_preload_err = magick_plugin
.with_limit()
:arg({
"-background",
"none",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url)
.. "["
.. tostring(layer_index)
.. "]",
"-auto-orient",
"-strip",
"-resize",
string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height),
"-quality",
rt.preview.image_quality,
string.format("PNG32:%s", tostring(tmp_file_path)),
})
:status()
if cache_img_status then
os.rename(tostring(tmp_file_path), tostring(cache_img_url))
end
elseif mime == "svg+xml" and not is_valid_utf8_path then
local cache_img_url_tmp = Url(cache_img_url .. ".tmp")
if fs.cha(cache_img_url_tmp) then
fs.remove("file", cache_img_url_tmp)
end
local tmp_file_path, _ = fs.unique_name(cache_img_url_tmp)
-- svg under invalid utf8 path
cache_img_status, image_preload_err = magick_plugin
.with_limit()
:arg({
"-background",
"none",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-auto-orient",
"-strip",
"-flatten",
"-resize",
string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height),
"-quality",
rt.preview.image_quality,
string.format("PNG32:%s", tostring(tmp_file_path)),
})
:status()
if cache_img_status then
os.rename(tostring(tmp_file_path), tostring(cache_img_url))
end
else
-- other image
local no_skip_job = { skip = 0, file = job.file, args = {} }
cache_img_status, image_preload_err = image_plugin:preload(no_skip_job)
end
if not cache_img_status then
err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "")
end
end
end
end
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local cmd = "mediainfo"
local output, err
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. path_quote(job.file.path or job.file.cache or (job.file.url.path or job.file.url).parent)
.. " && "
.. cmd
.. " "
.. path_quote(tostring(job.file.path or job.file.cache or job.file.url.name))
output, err = Command(SHELL)
:arg({ "-c", cmd })
:arg({ tostring(job.file.path or job.file.cache or (job.file.url.path or job.file.url)) })
:output()
end
if err then
err_msg = err_msg .. string.format("Failed to start `%s`, Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
function M:entry(job)
local action = job.args[1]
if action == ENTRY_ACTION.toggle_metadata then
set_state(STATE_KEY.hide_metadata, not get_state(STATE_KEY.hide_metadata))
M:re_peek()
end
end
return M
@@ -0,0 +1,149 @@
--- @since 26.1.22
local M = {}
local const = require(".const")
local utils = require(".utils")
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not (utils.get_state(const.STATE_KEY.skip_labels) or const.skip_labels)[label] then
line = ui.Line({
ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()),
ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")),
})
end
else
if not (utils.get_state(const.STATE_KEY.skip_section_labels) or {})[str] then
line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) })
end
end
if line then
local line_height = ui.height
and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap })
or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1))
if next_str == nil and line_height == 1 then
EOF_mediainfo = true
end
if (last_line + line_height) > job.skip then
table.insert(lines, line)
end
if (last_line + line_height) >= job.skip + limit then
last_line = job.skip + limit
EOF_mediainfo = false
break
end
last_line = last_line + line_height
end
str = next_str
end
end
if EOF_mediainfo and #lines == 0 and job.skip > 0 then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
end
utils.force_render()
-- Handle preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Clear(job.area),
ui.Text(lines):area(job.area):wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.STATE_KEY.prev_metadata_area, {
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = job.area.h,
win_x = job.area.x,
win_y = job.area.y,
win_w = job.area.w,
win_h = job.area.h,
} or nil)
end
function M:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M
@@ -0,0 +1,185 @@
--- @since 26.5.6
local M = {}
local const = require(".const")
local utils = require(".utils")
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
local opt = { ansi = true, tab_size = rt.preview.tab_size, wrap = rt.preview.wrap, width = max_width }
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not (utils.get_state(const.STATE_KEY.skip_labels) or const.skip_labels)[label] then
line = label .. ": " .. value
end
else
if not (utils.get_state(const.STATE_KEY.skip_section_labels) or {})[str] then
line = str
end
end
if line then
local wrapped = ui.lines(line, opt)
local line_height = #wrapped
local from = 1
local to = math.min(line_height, job.skip + limit - last_line)
local total_rendered_text_len = 1
local total_label_rendered_len = 1
local label_total_len = label and utf8.len(label .. ": ") or 0
for j = from, to do
local current_line_components = {}
local wrapped_line_len = wrapped[j]:width() or 0
local wrapped_raw =
utils.utf8_sub(line, total_rendered_text_len, total_rendered_text_len + wrapped_line_len)
wrapped_line_len = utf8.len(wrapped_raw)
total_rendered_text_len = total_rendered_text_len + wrapped_line_len
if last_line + 1 > job.skip then
local label_raw = label_total_len - total_label_rendered_len <= 0 and ""
or utils.utf8_sub(wrapped_raw, 1, label_total_len - total_label_rendered_len)
local label_raw_len = utf8.len(label_raw)
if label_raw_len > 0 then
table.insert(
current_line_components,
ui.Span(label_raw):style(ui.Style():fg("reset"):bold())
)
total_label_rendered_len = total_label_rendered_len + label_raw_len
wrapped_raw = wrapped_raw:gsub("^" .. utils.is_literal_string(label_raw), "", 1)
end
if total_label_rendered_len >= label_total_len then
local value_raw = wrapped_raw
table.insert(
current_line_components,
ui.Span(value_raw or ""):style(
label and (th.spot.tbl_col or ui.Style():fg("blue"))
or (th.spot.title or ui.Style():fg("green"))
)
)
end
table.insert(lines, ui.Line(current_line_components))
-- last_line = last_line + 1
else
total_label_rendered_len = total_label_rendered_len + total_rendered_text_len
end
last_line = last_line + 1
if next_str == nil and not wrapped[j + 1] then
EOF_mediainfo = true
end
if last_line >= job.skip + limit then
last_line = job.skip + limit
EOF_mediainfo = false
break
end
end
end
str = next_str
end
end
if EOF_mediainfo and #lines == 0 and job.skip > 0 then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
end
utils.force_render()
-- Handle preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Clear(job.area),
ui.Text(lines):area(job.area):wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.STATE_KEY.prev_metadata_area, {
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = job.area.h,
win_x = job.area.x,
win_y = job.area.y,
win_w = job.area.w,
win_h = job.area.h,
} or nil)
end
function M:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M
@@ -0,0 +1,127 @@
--- @since 26.1.22
local const = require(".const")
local M = {}
local MAX_ITEMS_IN_CACHE = 10
function M.is_valid_utf8(str)
return utf8.len(str) ~= nil
end
function M.utf8_sub(str, start_char, end_char)
local start_byte = utf8.offset(str, start_char) -- Expects start_char to be a character index
local end_byte = end_char and (utf8.offset(str, end_char + 1) or (#str + 1)) - 1 -- Expects end_char
if not start_byte then
return ""
end
return str:sub(start_byte, end_byte)
end
function M.is_literal_string(str)
return str and str:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
end
function M.path_quote(path)
if not path or tostring(path) == "" then
return path
end
local result = "'" .. string.gsub(tostring(path), "'", "'\\''") .. "'"
return result
end
M.force_render = ya.sync(function(_, _)
(ui.render or ya.render)()
end)
M.set_state = ya.sync(function(state, key, value)
state[key] = value
end)
M.get_state = ya.sync(function(state, key)
return state[key]
end)
M.set_states = ya.sync(function(state, namespace, key, value, limit_cached_items)
if not limit_cached_items then
limit_cached_items = MAX_ITEMS_IN_CACHE
end
if not state[namespace] then
state[namespace] = {}
end
local storage = state[namespace]
if limit_cached_items and #storage > limit_cached_items then
table.remove(storage, 1)
end
storage[key] = value
end)
M.get_states = ya.sync(function(state, namespace, key)
local storage = state[namespace]
if not storage then
return nil
end
if storage then
return storage[key]
end
end)
function M.read_mediainfo_cached_file(file_path)
local cached = M.get_states(const.STATE_KEY.cached_mediainfo, file_path)
if cached then
return cached
end
-- Open the file in read mode
local file = io.open(file_path, "r")
if file then
-- Read the entire file content
local content = file:read("*all")
file:close()
M.set_states(const.STATE_KEY.cached_mediainfo, file_path, content)
return content
end
end
M.current_file = ya.sync(function()
local h = cx.active.current.hovered
if not h then
return
end
return tostring(h.url)
end)
function M.error(s, ...)
ya.notify({ title = "mediainfo", content = string.format(s, ...), timeout = 3, level = "error" })
end
function M.info(s, ...)
ya.notify({ title = "mediainfo", content = string.format(s, ...), timeout = 3, level = "info" })
end
--- return a string array with unique value
---@param tbl string[]
---@return string[] table with only unique strings
function M.tbl_unique_strings(tbl)
local unique_table = {}
local seen = {}
for _, str in ipairs(tbl) do
if not seen[str] then
seen[str] = true
table.insert(unique_table, str)
end
end
return unique_table
end
function M.tbl_to_set(t1)
local set = {}
for _, v in ipairs(t1) do
set[v] = true
end
return set
end
return M
@@ -0,0 +1,241 @@
--- @since 26.1.22
local M = {}
local const = require(".const")
local utils = require(".utils")
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache({
skip = job.skip > 90 and 90 or job.skip,
args = job.args,
file = job.file,
area = job.area,
})
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local no_metadata = job.args.no_metadata
local mediainfo_job_skip = job.skip
::recalc_mediainfo_job_skip::
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not no_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not (utils.get_state(const.STATE_KEY.skip_labels) or const.skip_labels)[label] then
line = ui.Line({
ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()),
ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")),
})
end
else
if not (utils.get_state(const.STATE_KEY.skip_section_labels) or {})[str] then
line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) })
end
end
if line then
local line_height = ui.height
and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap })
or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1))
if next_str == nil and line_height == 1 then
EOF_mediainfo = true
end
if (last_line + line_height) > mediainfo_job_skip then
table.insert(lines, line)
end
if (last_line + line_height) >= mediainfo_job_skip + limit then
last_line = mediainfo_job_skip + limit
EOF_mediainfo = false
break
end
last_line = last_line + line_height
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not no_metadata then
if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then
if job.skip > 90 then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
else
-- NOTE: Recalculate mediainfo using cached latest valid skip value when reach the end of mediainfo output
local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip)
mediainfo_job_skip = last_valid_mediainfo_skip
and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)]
or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0))
goto recalc_mediainfo_job_skip
end
else
utils.set_state(
const.STATE_KEY.last_valid_mediainfo_skip,
{ [tostring(cache_img_url_no_skip)] = mediainfo_job_skip }
)
end
end
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
local old_metadata_area = utils.get_state(const.STATE_KEY.prev_metadata_area)
if
old_metadata_area.win_x == job.area.x
and old_metadata_area.win_y == job.area.y
and old_metadata_area.win_w == job.area.w
and old_metadata_area.win_h == job.area.h
then
ya.preview_widget(job, {
ui.Clear(ui.Rect({
x = old_metadata_area.x,
y = old_metadata_area.y,
w = old_metadata_area.w,
h = old_metadata_area.h,
})),
})
end
end
utils.force_render()
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- NOTE: Workaround case video.lua doesn't doesn't generate preview image because of `skip` overflow video duration
if not rendered_img_rect then
local prev_image_height = utils.get_state(const.STATE_KEY.prev_image_height)
image_height = prev_image_height and prev_image_height[tostring(cache_img_url_no_skip)] or 0
else
utils.set_state(const.STATE_KEY.prev_image_height, { [tostring(cache_img_url_no_skip)] = image_height })
end
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.STATE_KEY.prev_metadata_area, not no_metadata and {
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
win_x = job.area.x,
win_y = job.area.y,
win_w = job.area.w,
win_h = job.area.h,
} or nil)
end
function M:preload(job)
local cmd = "mediainfo"
local err_msg = ""
-- NOTE: Preload image from video
local cache_img_status, video_preload_err = require("video"):preload({
skip = job.skip > 90 and 90 or job.skip,
args = job.args,
file = job.file,
area = job.area,
})
if not cache_img_status and video_preload_err then
ya.dbg("mediainfo", video_preload_err)
err_msg = err_msg .. string.format("Failed to start `%s`.\n Do you have `%s` installed?\n", "ffmpeg", "ffmpeg")
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M
@@ -0,0 +1,277 @@
--- @since 26.5.6
local M = {}
local const = require(".const")
local utils = require(".utils")
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache({
skip = job.skip > 90 and 90 or job.skip,
args = job.args,
file = job.file,
area = job.area,
})
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local no_metadata = job.args.no_metadata
local mediainfo_job_skip = job.skip
::recalc_mediainfo_job_skip::
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not no_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
local opt = { ansi = true, tab_size = rt.preview.tab_size, wrap = rt.preview.wrap, width = max_width }
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not (utils.get_state(const.STATE_KEY.skip_labels) or const.skip_labels)[label] then
line = label .. ": " .. value
end
else
if not (utils.get_state(const.STATE_KEY.skip_section_labels) or {})[str] then
line = str
end
end
if line then
local wrapped = ui.lines(line, opt)
local line_height = #wrapped
local from = 1
local to = math.min(line_height, mediainfo_job_skip + limit - last_line)
local total_rendered_text_len = 1
local total_label_rendered_len = 1
local label_total_len = label and utf8.len(label .. ": ") or 0
for j = from, to do
local current_line_components = {}
local wrapped_line_len = wrapped[j]:width() or 0
local wrapped_raw =
utils.utf8_sub(line, total_rendered_text_len, total_rendered_text_len + wrapped_line_len)
wrapped_line_len = utf8.len(wrapped_raw)
total_rendered_text_len = total_rendered_text_len + wrapped_line_len
if last_line + 1 > mediainfo_job_skip then
local label_raw = label_total_len - total_label_rendered_len <= 0 and ""
or utils.utf8_sub(wrapped_raw, 1, label_total_len - total_label_rendered_len)
local label_raw_len = utf8.len(label_raw)
if label_raw_len > 0 then
table.insert(
current_line_components,
ui.Span(label_raw):style(ui.Style():fg("reset"):bold())
)
total_label_rendered_len = total_label_rendered_len + label_raw_len
wrapped_raw = wrapped_raw:gsub("^" .. utils.is_literal_string(label_raw), "", 1)
end
if total_label_rendered_len >= label_total_len then
local value_raw = wrapped_raw
table.insert(
current_line_components,
ui.Span(value_raw or ""):style(
label and (th.spot.tbl_col or ui.Style():fg("blue"))
or (th.spot.title or ui.Style():fg("green"))
)
)
end
table.insert(lines, ui.Line(current_line_components))
-- last_line = last_line + 1
else
total_label_rendered_len = total_label_rendered_len + total_rendered_text_len
end
last_line = last_line + 1
if next_str == nil and not wrapped[j + 1] then
EOF_mediainfo = true
end
if last_line >= mediainfo_job_skip + limit then
last_line = mediainfo_job_skip + limit
EOF_mediainfo = false
break
end
end
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not no_metadata then
if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then
if job.skip > 90 then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
else
-- NOTE: Recalculate mediainfo using cached latest valid skip value when reach the end of mediainfo output
local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip)
mediainfo_job_skip = last_valid_mediainfo_skip
and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)]
or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0))
goto recalc_mediainfo_job_skip
end
else
utils.set_state(
const.STATE_KEY.last_valid_mediainfo_skip,
{ [tostring(cache_img_url_no_skip)] = mediainfo_job_skip }
)
end
end
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
local old_metadata_area = utils.get_state(const.STATE_KEY.prev_metadata_area)
if
old_metadata_area.win_x == job.area.x
and old_metadata_area.win_y == job.area.y
and old_metadata_area.win_w == job.area.w
and old_metadata_area.win_h == job.area.h
then
ya.preview_widget(job, {
ui.Clear(ui.Rect({
x = old_metadata_area.x,
y = old_metadata_area.y,
w = old_metadata_area.w,
h = old_metadata_area.h,
})),
})
end
end
utils.force_render()
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- NOTE: Workaround case video.lua doesn't doesn't generate preview image because of `skip` overflow video duration
if not rendered_img_rect then
local prev_image_height = utils.get_state(const.STATE_KEY.prev_image_height)
image_height = prev_image_height and prev_image_height[tostring(cache_img_url_no_skip)] or 0
else
utils.set_state(const.STATE_KEY.prev_image_height, { [tostring(cache_img_url_no_skip)] = image_height })
end
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.STATE_KEY.prev_metadata_area, not no_metadata and {
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
win_x = job.area.x,
win_y = job.area.y,
win_w = job.area.w,
win_h = job.area.h,
} or nil)
end
function M:preload(job)
local cmd = "mediainfo"
local err_msg = ""
-- NOTE: Preload image from video
local cache_img_status, video_preload_err = require("video"):preload({
skip = job.skip > 90 and 90 or job.skip,
args = job.args,
file = job.file,
area = job.area,
})
if not cache_img_status and video_preload_err then
ya.dbg("mediainfo", video_preload_err)
err_msg = err_msg .. string.format("Failed to start `%s`.\n Do you have `%s` installed?\n", "ffmpeg", "ffmpeg")
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
if is_valid_utf8_path then
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M