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:
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
|
||||
|
||||

|
||||
|
||||
- Audio file with cover
|
||||
|
||||

|
||||
|
||||
- Images
|
||||
|
||||

|
||||
|
||||
- Subtitle
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
Reference in new issue
Block a user