update
This commit is contained in:
parent
4524a86376
commit
0552ffdae8
37 changed files with 8770 additions and 49 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@ config/spicetify/Extracted
|
||||||
config/vifm/Trash
|
config/vifm/Trash
|
||||||
config/vifm/vifminfo.json
|
config/vifm/vifminfo.json
|
||||||
config/lazygit/state.yml
|
config/lazygit/state.yml
|
||||||
|
config/mpv/shaders
|
||||||
|
|
|
||||||
49
config/fzf/forgitrc
Normal file
49
config/fzf/forgitrc
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
forgit-list-command-widget() {
|
||||||
|
local cmd=$(alias | grep 'forgit' | sed -E 's/::/ /g; s/=/ :- /g; s/for//g' | \
|
||||||
|
awk -v BLD=${BLD} -v RST=${RST} -v BLU=${BLU} -v CYN=${CYN} -F":" '{print BLU BLD $1 RST ":" CYN $2 RST }' | \
|
||||||
|
column -ts":")
|
||||||
|
LBUFFER="$LBUFFER$( \
|
||||||
|
fzf-tmux -p -w 30% -h 53% --preview-window=hidden --prompt=' List > ' \
|
||||||
|
--header=$'Alias\t Description' <<< "$cmd" | awk '{print $1}')"
|
||||||
|
if [[ -n $LBUFFER ]]; then
|
||||||
|
zle accept-line
|
||||||
|
fi
|
||||||
|
zle reset-prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
zle -N forgit-list-command-widget
|
||||||
|
bindkey '^[g^[l' forgit-list-command-widget #<Alt-G+Alt-L>
|
||||||
|
|
||||||
|
export FORGIT_FZF_SHOW_HELP_OPTS="$(
|
||||||
|
cat <<-EOF
|
||||||
|
|
||||||
|
Forgit Commands (Aliases)
|
||||||
|
|
||||||
|
fga add
|
||||||
|
fgbl blame
|
||||||
|
fgbd branch delete
|
||||||
|
fgcb checkout branch
|
||||||
|
fgco checkout commit
|
||||||
|
fgcf checkout file
|
||||||
|
fgct checkout tag
|
||||||
|
fgcp cherry pick
|
||||||
|
fgclean clean
|
||||||
|
fgd diff
|
||||||
|
fgfu fixup
|
||||||
|
fgi ignore
|
||||||
|
fglo log
|
||||||
|
fgrb rebase
|
||||||
|
fgrh reset head
|
||||||
|
fgrev revert commit
|
||||||
|
fgsta stash push
|
||||||
|
fgsts stash show
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
export FORGIT_FZF_DEFAULT_OPTS="
|
||||||
|
--cycle
|
||||||
|
--reverse
|
||||||
|
--height '80%'
|
||||||
|
--preview-window=nohidden
|
||||||
|
--bind 'alt-?:preview(printf \"${FORGIT_FZF_SHOW_HELP_OPTS}\")'
|
||||||
|
"
|
||||||
130
config/fzf/fzfrc
Normal file
130
config/fzf/fzfrc
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
export FZF_SHOW_HELP_OPTS="$(
|
||||||
|
cat <<-EOF
|
||||||
|
|
||||||
|
FZF Keybinds Shortcut
|
||||||
|
|
||||||
|
? Toggle/Hide Preview
|
||||||
|
C-space Change preview layout
|
||||||
|
C-e Open in Editor
|
||||||
|
C-v Open in VsCode
|
||||||
|
C-o Launch Application Chooser
|
||||||
|
M-o Open in Default Appllication
|
||||||
|
C-/ Directory: Navigate on broot
|
||||||
|
C-/ File: Open in Pager (bat)
|
||||||
|
|
||||||
|
M-s Toggle Sort
|
||||||
|
C-y Copy/Yank
|
||||||
|
C-M-y Copy/Yank Working Directory
|
||||||
|
C-a Select all
|
||||||
|
C-M-d Deselect All
|
||||||
|
Del Delete/Remove file
|
||||||
|
|
||||||
|
Alt-? Help (this page)
|
||||||
|
ESC Quit
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
export FZF_THEME_CATPPUCCIN_MOCHA="
|
||||||
|
--color=bg+:#313244,bg:#1e1e2e,spinner:#f5e0dc,hl:#f38ba8 \
|
||||||
|
--color=fg:#cdd6f4,header:#f38ba8,info:#cba6f7,pointer:#f5e0dc \
|
||||||
|
--color=marker:#f5e0dc,fg+:blue,border:blue,prompt:#cba6f7,hl+:#f38ba8"
|
||||||
|
|
||||||
|
export FZF_PREVIEW_OPTS="--preview
|
||||||
|
'([[ {} =~ ('.jpg'|'.jpeg'|'.png'|'.gif'|'.bmp'|'.svg'|'.mp4'|'.mkv')$ ]] && (chafa --center=on {} && exiftool {})) ||
|
||||||
|
([[ -f {} ]] && (bat --style=header,numbers,changes,plain --color=always --language=sh --line-range :500 {} || cat {})) ||
|
||||||
|
([[ -d {} ]] && (lsd -all --long --tree --depth=5 --group-dirs=first -I=.git {} )) || echo {} 3>/dev/null | head -n 500'
|
||||||
|
"
|
||||||
|
|
||||||
|
#'([[ \$(file -bL --mime-type {} 2> /dev/null = image) ]] && (catimg -w 100 {})) || # throwing an stb error cant silence
|
||||||
|
|
||||||
|
export FZF_PREVIEW_KEYBIND_OPTS="
|
||||||
|
--bind '?:toggle-preview'
|
||||||
|
--bind 'alt-?:preview(printf \"${FZF_SHOW_HELP_OPTS}\")'
|
||||||
|
--bind 'alt-j:preview-down'
|
||||||
|
--bind 'alt-k:preview-up'
|
||||||
|
--bind 'ctrl-d:preview-page-down'
|
||||||
|
--bind 'ctrl-u:preview-page-up'
|
||||||
|
--bind 'ctrl-t:preview-top'
|
||||||
|
--bind 'ctrl-b:preview-bottom'
|
||||||
|
--bind 'ctrl-l:clear-screen+clear-query+first'
|
||||||
|
--bind 'ctrl-space:change-preview-window(right,80%,nohidden|down,80%,border-top,nohidden|down,50%,nohidden|up,80%,border-down,nohidden|up,50%,nohidden|left,80%,nohidden|left,50%,nohidden|down:3:nohidden:wrap|up:3,nohidden:wrap|right,50%,nohidden)'
|
||||||
|
"
|
||||||
|
export FZF_KEYBIND_SHORTCUTS="
|
||||||
|
$FZF_PREVIEW_KEYBIND_OPTS
|
||||||
|
--bind 'alt-o:execute(xdg-open {+})'
|
||||||
|
--bind 'alt-s:toggle-sort'
|
||||||
|
--bind 'ctrl-/:execute(
|
||||||
|
if [[ -d {} ]]; then
|
||||||
|
broot {} < /dev/tty > /dev/tty 2>&1
|
||||||
|
elif [[ {} =~ ('.jpg'|'.jpeg'|'.png'|'.gif'|'.bmp'|'.svg'|'.mp4'|'.mkv')$ ]]; then
|
||||||
|
chafa --center {} | less > /dev/tty
|
||||||
|
else
|
||||||
|
bat --paging=always --style=plain --color=always --language=sh {} > /dev/tty
|
||||||
|
fi)'
|
||||||
|
--bind 'ctrl-a:select-all'
|
||||||
|
--bind 'ctrl-alt-d:deselect-all'
|
||||||
|
--bind 'ctrl-o:execute(flatpak run re.sonny.Junction {+})'
|
||||||
|
--bind 'ctrl-y:execute-silent(wl-copy {+})'
|
||||||
|
--bind 'ctrl-alt-y:execute-silent(readlink -f {+} | wl-copy)'
|
||||||
|
--bind 'ctrl-e:execute(${EDITOR} {} > /dev/tty)'
|
||||||
|
--bind 'ctrl-v:execute(code {+})'
|
||||||
|
--bind 'del:execute(rm -iv {};)+reload($FZF_DEFAULT_COMMAND)+clear-screen'
|
||||||
|
"
|
||||||
|
# --bind 'ctrl-/:execute(if [[ -f {} ]]; then bat --paging=always --style=\"header,numbers,changes\" --language=sh {} < /dev/tty > /dev/tty 2>&1; else broot {} < /dev/tty > /dev/tty 2>&1
|
||||||
|
# ; fi)'
|
||||||
|
|
||||||
|
export FZF_DEFAULT_COMMAND="fd --color=always --hidden --exclude .git"
|
||||||
|
|
||||||
|
export FZF_DEFAULT_OPTS="
|
||||||
|
"$FZF_PREVIEW_OPTS"
|
||||||
|
"$FZF_KEYBIND_SHORTCUTS"
|
||||||
|
"$FZF_THEME_CATPPUCCIN_MOCHA"
|
||||||
|
-i
|
||||||
|
--ansi
|
||||||
|
--multi
|
||||||
|
--height=90%
|
||||||
|
--info=inline
|
||||||
|
--no-separator
|
||||||
|
--layout=reverse
|
||||||
|
--preview-window=:hidden
|
||||||
|
"
|
||||||
|
|
||||||
|
export FZF_ALT_C_COMMAND="fd --type=d --color=always --hidden --exclude .git"
|
||||||
|
|
||||||
|
export FZF_ALT_C_OPTS="
|
||||||
|
--preview 'lsd --all --long --tree --depth=3 {} | head -500'
|
||||||
|
--preview-window 'nohidden,<50(down,75%,border-top)'
|
||||||
|
--bind 'alt-h:reload(fd --type=d --color=always --follow --exclude .git)'
|
||||||
|
--bind 'alt-c:reload(fd -p ~ --color=always --hidden --type=d --follow)'
|
||||||
|
"
|
||||||
|
|
||||||
|
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
|
||||||
|
|
||||||
|
export FZF_CTRL_T_OPTS="
|
||||||
|
--exit-0
|
||||||
|
--select-1
|
||||||
|
--info=default
|
||||||
|
--layout=reverse-list
|
||||||
|
--preview-window '50%,<50(up,75%,border-down)'
|
||||||
|
--header 'Alt-D: Directories | Alt-F: Files | Alt-H: Hide Files'
|
||||||
|
--bind 'alt-d:change-prompt( Directories > )+reload("$FZF_ALT_C_COMMAND")'
|
||||||
|
--bind 'alt-f:change-prompt( Files > )+reload("$FZF_DEFAULT_COMMAND")'
|
||||||
|
--bind 'alt-h:change-prompt( Hide Files > )+reload(fd --type=f --color=always --follow)'
|
||||||
|
--bind 'ctrl-t:change-prompt(Home > )+reload(fd --base-directory ~ --color=always --hidden --exclude .git)'
|
||||||
|
"
|
||||||
|
|
||||||
|
export FZF_CTRL_R_OPTS="
|
||||||
|
--preview 'echo {+} | bat --color=always --wrap never --language=sh --style=plain'
|
||||||
|
--preview-window 'down:3:nohidden:wrap'"
|
||||||
|
|
||||||
|
# export FZF_TMUX_OPTS='-p80% --color=border:blue'
|
||||||
|
# FZF_TMUX_CTRL_R_OPT="fzf-tmux -p $FZF_CTRL_R_OPTS"
|
||||||
|
# --bind 'alt-p:execute($'FZF_TMUX_OPTS'='-p90% --color=border:blue')'
|
||||||
|
|
||||||
|
# fzf completion '**' doesn't preview files (idk if it is a bug)
|
||||||
|
_fzf_compgen_path() {
|
||||||
|
fd --color=always --hidden --follow --exclude ".git" . "$1"
|
||||||
|
}
|
||||||
|
_fzf_compgen_dir() {
|
||||||
|
fd --color=always --type d --hidden --follow --exclude ".git" . "$1"
|
||||||
|
}
|
||||||
16
config/fzf/widgets/alias_widget.zsh
Normal file
16
config/fzf/widgets/alias_widget.zsh
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
fzf-aliases-widget() {
|
||||||
|
LBUFFER="$LBUFFER$(FZF_DEFAULT_COMMAND=
|
||||||
|
alias | sed 's/=/ --- /' | \
|
||||||
|
awk -v blu=$(tput setaf 4) -v cyn=$(tput setaf 6) -v bld=$(tput bold) -v rst=$(tput sgr0) -F '---' \
|
||||||
|
'{
|
||||||
|
print bld cyn $1 rst blu "--" $2
|
||||||
|
}' | \
|
||||||
|
tr -d "'" | column -tl2 | \
|
||||||
|
fzf --prompt=" Aliases > " \
|
||||||
|
--preview 'echo {3..} | bat --color=always --plain --language=sh' \
|
||||||
|
--preview-window 'up:4:nohidden:wrap' | cut -d' ' -f 1)"
|
||||||
|
zle reset-prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
zle -N fzf-aliases-widget
|
||||||
|
bindkey '^[a' fzf-aliases-widget #<Alt-A>
|
||||||
25
config/fzf/widgets/atuin-history_widget.zsh
Normal file
25
config/fzf/widgets/atuin-history_widget.zsh
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
fzf-atuin-history-widget() {
|
||||||
|
local selected num
|
||||||
|
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
|
||||||
|
selected=( $(atuin history list --cmd-only | tac | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' | bat --color=always --wrap never --language=sh --style=plain |
|
||||||
|
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
|
||||||
|
local ret=$?
|
||||||
|
if [ -n "$selected" ]; then
|
||||||
|
cmd=$selected[1,-1]
|
||||||
|
if [ -n "$cmd" ]; then
|
||||||
|
zle vi-fetch-history -n $cmd
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
zle -U "$cmd"
|
||||||
|
zle kill-buffer
|
||||||
|
zle reset-prompt
|
||||||
|
return $ret
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v atuin > /dev/null; then
|
||||||
|
zle -N fzf-history-widget
|
||||||
|
bindkey '^R' fzf-history-widget
|
||||||
|
else
|
||||||
|
zle -N fzf-atuin-history-widget
|
||||||
|
bindkey '^R' fzf-atuin-history-widget
|
||||||
|
fi
|
||||||
15
config/fzf/widgets/cd-recent-dir_widget.zsh
Normal file
15
config/fzf/widgets/cd-recent-dir_widget.zsh
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
fzf-cd-recent-dir-widget () {
|
||||||
|
local dir
|
||||||
|
print -rNC1 -- $dirstack |
|
||||||
|
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m \
|
||||||
|
--color=fg:bold:blue --query=${LBUFFER} --read0 --print0 |
|
||||||
|
IFS= read -rd '' dir
|
||||||
|
if [[ -n $dir ]]; then
|
||||||
|
BUFFER=" builtin cd -- $dir"
|
||||||
|
zle accept-line
|
||||||
|
fi
|
||||||
|
zle reset-prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
zle -N fzf-cd-recent-dir-widget
|
||||||
|
bindkey '^[C' fzf-cd-recent-dir-widget #<Alt-Shift-C>
|
||||||
20
config/fzf/widgets/cd_widget.zsh
Normal file
20
config/fzf/widgets/cd_widget.zsh
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
fzf-cd-widget() {
|
||||||
|
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||||
|
-o -type d -print 2> /dev/null | cut -b3-"}"
|
||||||
|
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||||
|
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m)"
|
||||||
|
if [[ -z "$dir" ]]; then
|
||||||
|
zle redisplay
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
zle push-line # Clear buffer. Auto-restored on next prompt.
|
||||||
|
BUFFER=" builtin cd -- ${(q)dir}"
|
||||||
|
zle accept-line
|
||||||
|
local ret=$?
|
||||||
|
unset dir # ensure this doesn't end up appearing in prompt expansion
|
||||||
|
zle reset-prompt
|
||||||
|
return $ret
|
||||||
|
}
|
||||||
|
|
||||||
|
zle -N fzf-cd-widget
|
||||||
|
bindkey '^[c' fzf-cd-widget #<Alt-C>
|
||||||
24
config/fzf/widgets/dictionary_widget.zsh
Normal file
24
config/fzf/widgets/dictionary_widget.zsh
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
fzf-dictionary-widget() {
|
||||||
|
local dict wiki wikis gogl
|
||||||
|
dict="dict {}"
|
||||||
|
wiki="wiki {} > /dev/tty"
|
||||||
|
wweb="w3m https://en.wikipedia.org/wiki/{}"
|
||||||
|
gogl="w3m https://google.com/search?q=define\ {}"
|
||||||
|
LBUFFER="$LBUFFER$(FZF_DEFAULT_COMMAND= cat /usr/share/dict/*words | sort | uniq -id | \
|
||||||
|
fzf-tmux \
|
||||||
|
-p60% \
|
||||||
|
--layout=default \
|
||||||
|
--header-first \
|
||||||
|
--header="M-w: Wiki | M-d: Define | M-g: Google" \
|
||||||
|
--color=fg:blue,fg+:blue,border:blue \
|
||||||
|
--bind="alt-d:change-preview($dict)" \
|
||||||
|
--bind="alt-w:execute($wiki)" \
|
||||||
|
--bind="alt-g:execute($gogl)" \
|
||||||
|
--prompt=" > " \
|
||||||
|
--preview "$dict" \
|
||||||
|
--preview-window='up,85%,border-bottom,wrap' | paste -sd" " -)"
|
||||||
|
zle reset-prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
zle -N fzf-dictionary-widget
|
||||||
|
bindkey '^[d' fzf-dictionary-widget #<Alt-D>
|
||||||
7
config/fzf/widgets/fzf-rg-launcher.zsh
Normal file
7
config/fzf/widgets/fzf-rg-launcher.zsh
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
function fzf-rg-widget() {
|
||||||
|
bash ${DOOTS:-$HOME/.local}/bin/fzf-rg-launcher "$LBUFFER"
|
||||||
|
zle redisplay
|
||||||
|
}
|
||||||
|
|
||||||
|
zle -N fzf-rg-widget
|
||||||
|
bindkey '^F' fzf-rg-widget
|
||||||
13
config/fzf/widgets/locate_widget.zsh
Normal file
13
config/fzf/widgets/locate_widget.zsh
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
fzf-locate-widget() {
|
||||||
|
local selected
|
||||||
|
if selected=$(locate / | fzf --prompt " Locate > " -q "$LBUFFER" \
|
||||||
|
--bind 'alt-u:execute(sudo updatedb)' --header 'M-u: UpdateDB' \
|
||||||
|
--color=fg:bold:blue --preview-window '<50(down,75%,border-top)'
|
||||||
|
); then
|
||||||
|
LBUFFER=$selected
|
||||||
|
fi
|
||||||
|
zle reset-prompt
|
||||||
|
}
|
||||||
|
zle -N fzf-locate-widget
|
||||||
|
bindkey '^[i' fzf-locate-widget #<Alt-I>
|
||||||
|
|
||||||
21
config/fzf/widgets/man_widget.zsh
Normal file
21
config/fzf/widgets/man_widget.zsh
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
fzf-man-widget() {
|
||||||
|
batman="man {1} | col -bx | bat --language=man --plain --color always --theme=\"Monokai Extended\""
|
||||||
|
man -k . | sort \
|
||||||
|
| awk -v CYN=${CYN} -v BLU=${BLU} -v RES=${RES} -v BLD=${BLD} '{ $1=CYN BLD $1; $2=RES BLU;} 1' \
|
||||||
|
| fzf \
|
||||||
|
-q "$LBUFFER" \
|
||||||
|
--ansi \
|
||||||
|
--tiebreak=begin \
|
||||||
|
--prompt=' Man > ' \
|
||||||
|
--header="M-u: update mandb | M-t: tl;dr | M-c: cheat.sh | M:m manual " \
|
||||||
|
--preview-window '50%,rounded,<50(down,80%,border-up)' \
|
||||||
|
--preview "${batman}" \
|
||||||
|
--bind "enter:execute(man {1})" \
|
||||||
|
--bind "alt-c:+change-preview(curl -s cht.sh/{1})+change-prompt(ﯽ Cheat > )" \
|
||||||
|
--bind "alt-m:+change-preview(${batman})+change-prompt( Man > )" \
|
||||||
|
--bind "alt-u:execute(sudo mandb && echo -e '\nUpdating tl;dr cache...';tldr --update)" \
|
||||||
|
--bind "alt-t:+change-preview(tldr --color=always {1})+change-prompt(ﳁ TLDR > )"
|
||||||
|
zle reset-prompt
|
||||||
|
}
|
||||||
|
zle -N fzf-man-widget
|
||||||
|
bindkey '^[h' fzf-man-widget
|
||||||
155
config/mpv/encoding.rst
Normal file
155
config/mpv/encoding.rst
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
General usage
|
||||||
|
=============
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mpv infile -o outfile [-of outfileformat] [-ofopts formatoptions] [-orawts] \
|
||||||
|
[(any other mpv options)] \
|
||||||
|
-ovc outvideocodec [-ovcopts outvideocodecoptions] \
|
||||||
|
-oac outaudiocodec [-oacopts outaudiocodecoptions]
|
||||||
|
|
||||||
|
Help for these options is provided if giving help as parameter, as in::
|
||||||
|
|
||||||
|
mpv -ovc help
|
||||||
|
|
||||||
|
The suboptions of these generally are identical to ffmpeg's (as option parsing
|
||||||
|
is simply delegated to ffmpeg). The option -ocopyts enables copying timestamps
|
||||||
|
from the source as-is, instead of fixing them to match audio playback time
|
||||||
|
(note: this doesn't work with all output container formats); -orawts even turns
|
||||||
|
off discontinuity fixing.
|
||||||
|
|
||||||
|
Note that if neither -ofps nor -oautofps is specified, VFR encoding is assumed
|
||||||
|
and the time base is 24000fps. -oautofps sets -ofps to a guessed fps number
|
||||||
|
from the input video. Note that not all codecs and not all formats support VFR
|
||||||
|
encoding, and some which do have bugs when a target bitrate is specified - use
|
||||||
|
-ofps or -oautofps to force CFR encoding in these cases.
|
||||||
|
|
||||||
|
Of course, the options can be stored in a profile, like this .config/mpv/mpv.conf
|
||||||
|
section::
|
||||||
|
|
||||||
|
[myencprofile]
|
||||||
|
vf-add = scale=480:-2
|
||||||
|
ovc = libx264
|
||||||
|
ovcopts-add = preset=medium
|
||||||
|
ovcopts-add = tune=fastdecode
|
||||||
|
ovcopts-add = crf=23
|
||||||
|
ovcopts-add = maxrate=1500k
|
||||||
|
ovcopts-add = bufsize=1000k
|
||||||
|
ovcopts-add = rc_init_occupancy=900k
|
||||||
|
ovcopts-add = refs=2
|
||||||
|
ovcopts-add = profile=baseline
|
||||||
|
oac = aac
|
||||||
|
oacopts-add = b=96k
|
||||||
|
|
||||||
|
It's also possible to define default encoding options by putting them into
|
||||||
|
the section named ``[encoding]``. (This behavior changed after mpv 0.3.x. In
|
||||||
|
mpv 0.3.x, config options in the default section / no section were applied
|
||||||
|
to encoding. This is not the case anymore.)
|
||||||
|
|
||||||
|
One can then encode using this profile using the command::
|
||||||
|
|
||||||
|
mpv infile -o outfile.mp4 -profile myencprofile
|
||||||
|
|
||||||
|
Some example profiles are provided in a file
|
||||||
|
etc/encoding-profiles.conf; as for this, see below.
|
||||||
|
|
||||||
|
|
||||||
|
Encoding examples
|
||||||
|
=================
|
||||||
|
|
||||||
|
These are some examples of encoding targets this code has been used and tested
|
||||||
|
for.
|
||||||
|
|
||||||
|
Typical MPEG-4 Part 2 ("ASP", "DivX") encoding, AVI container::
|
||||||
|
|
||||||
|
mpv infile -o outfile.avi \
|
||||||
|
--vf=fps=25 \
|
||||||
|
-ovc mpeg4 -ovcopts qscale=4 \
|
||||||
|
-oac libmp3lame -oacopts ab=128k
|
||||||
|
|
||||||
|
Note: AVI does not support variable frame rate, so the fps filter must be used.
|
||||||
|
The frame rate should ideally match the input (25 for PAL, 24000/1001 or
|
||||||
|
30000/1001 for NTSC)
|
||||||
|
|
||||||
|
Typical MPEG-4 Part 10 ("AVC", "H.264") encoding, Matroska (MKV) container::
|
||||||
|
|
||||||
|
mpv infile -o outfile.mkv \
|
||||||
|
-ovc libx264 -ovcopts preset=medium,crf=23,profile=baseline \
|
||||||
|
-oac libvorbis -oacopts qscale=3
|
||||||
|
|
||||||
|
Typical MPEG-4 Part 10 ("AVC", "H.264") encoding, MPEG-4 (MP4) container::
|
||||||
|
|
||||||
|
mpv infile -o outfile.mp4 \
|
||||||
|
-ovc libx264 -ovcopts preset=medium,crf=23,profile=baseline \
|
||||||
|
-oac aac -oacopts ab=128k
|
||||||
|
|
||||||
|
Typical VP8 encoding, WebM (restricted Matroska) container::
|
||||||
|
|
||||||
|
mpv infile -o outfile.mkv \
|
||||||
|
-of webm \
|
||||||
|
-ovc libvpx -ovcopts qmin=6,b=1000000k \
|
||||||
|
-oac libvorbis -oacopts qscale=3
|
||||||
|
|
||||||
|
|
||||||
|
Device targets
|
||||||
|
==============
|
||||||
|
|
||||||
|
As the options for various devices can get complex, profiles can be used.
|
||||||
|
|
||||||
|
An example profile file for encoding is provided in
|
||||||
|
etc/encoding-profiles.conf in the source tree. This file is installed and loaded
|
||||||
|
by default. If you want to modify it, you can replace and it with your own copy
|
||||||
|
by doing::
|
||||||
|
|
||||||
|
mkdir -p ~/.mpv
|
||||||
|
cp /etc/mpv/encoding-profiles.conf ~/.mpv/encoding-profiles.conf
|
||||||
|
|
||||||
|
Keep in mind that the default profile is the playback one. If you want to add
|
||||||
|
options that apply only in encoding mode, put them into a ``[encoding]``
|
||||||
|
section.
|
||||||
|
|
||||||
|
Refer to the top of that file for more comments - in a nutshell, the following
|
||||||
|
options are added by it::
|
||||||
|
|
||||||
|
-profile enc-to-dvdpal DVD-Video PAL, use dvdauthor -v pal+4:3 -a ac3+en
|
||||||
|
-profile enc-to-dvdntsc DVD-Video NTSC, use dvdauthor -v ntsc+4:3 -a ac3+en
|
||||||
|
-profile enc-to-bb-9000 MP4 for Blackberry Bold 9000
|
||||||
|
-profile enc-to-nok-6300 3GP for Nokia 6300
|
||||||
|
-profile enc-to-psp MP4 for PlayStation Portable
|
||||||
|
-profile enc-to-iphone MP4 for iPhone
|
||||||
|
-profile enc-to-iphone-4 MP4 for iPhone 4 (double res)
|
||||||
|
-profile enc-to-iphone-5 MP4 for iPhone 5 (even larger res)
|
||||||
|
|
||||||
|
You can encode using these with a command line like::
|
||||||
|
|
||||||
|
mpv infile -o outfile.mp4 -profile enc-to-bb-9000
|
||||||
|
|
||||||
|
Of course, you are free to override options set by these profiles by specifying
|
||||||
|
them after the -profile option.
|
||||||
|
|
||||||
|
|
||||||
|
What works
|
||||||
|
==========
|
||||||
|
|
||||||
|
* Encoding at variable frame rate (default)
|
||||||
|
* Encoding at constant frame rate using --vf=fps=RATE
|
||||||
|
* 2-pass encoding (specify flags=+pass1 in the first pass's -ovcopts, specify
|
||||||
|
flags=+pass2 in the second pass)
|
||||||
|
* Hardcoding subtitles using vobsub, ass or srt subtitle rendering (just
|
||||||
|
configure mpv for the subtitles as usual)
|
||||||
|
* Hardcoding any other mpv OSD (e.g. time codes, using -osdlevel 3 and -vf
|
||||||
|
expand=::::1)
|
||||||
|
* Encoding directly from a DVD, network stream, webcam, or any other source
|
||||||
|
mpv supports
|
||||||
|
* Using x264 presets/tunings/profiles (by using profile=, tune=, preset= in the
|
||||||
|
-ovcopts)
|
||||||
|
* Deinterlacing/Inverse Telecine with any of mpv's filters for that
|
||||||
|
* Audio file converting: mpv -o outfile.mp3 infile.flac -no-video -oac
|
||||||
|
libmp3lame -oacopts ab=320k
|
||||||
|
|
||||||
|
What does not work yet
|
||||||
|
======================
|
||||||
|
|
||||||
|
* 3-pass encoding (ensuring constant total size and bitrate constraints while
|
||||||
|
having VBR audio; mencoder calls this "frameno")
|
||||||
|
* Direct stream copy
|
||||||
191
config/mpv/input.conf
Normal file
191
config/mpv/input.conf
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
# mpv keybindings
|
||||||
|
#
|
||||||
|
# Location of user-defined bindings: ~/.config/mpv/input.conf
|
||||||
|
#
|
||||||
|
# Lines starting with # are comments. Use SHARP to assign the # key.
|
||||||
|
# Copy this file and uncomment and edit the bindings you want to change.
|
||||||
|
#
|
||||||
|
# List of commands and further details: DOCS/man/input.rst
|
||||||
|
# List of special keys: --input-keylist
|
||||||
|
# Keybindings testing mode: mpv --input-test --force-window --idle
|
||||||
|
#
|
||||||
|
# Use 'ignore' to unbind a key fully (e.g. 'ctrl+a ignore').
|
||||||
|
#
|
||||||
|
# Strings need to be quoted and escaped:
|
||||||
|
# KEY show-text "This is a single backslash: \\ and a quote: \" !"
|
||||||
|
#
|
||||||
|
# You can use modifier-key combinations like Shift+Left or Ctrl+Alt+x with
|
||||||
|
# the modifiers Shift, Ctrl, Alt and Meta (may not work on the terminal).
|
||||||
|
#
|
||||||
|
# The default keybindings are hardcoded into the mpv binary.
|
||||||
|
# You can disable them completely with: --no-input-default-bindings
|
||||||
|
|
||||||
|
# Developer note:
|
||||||
|
# On compilation, this file is baked into the mpv binary, and all lines are
|
||||||
|
# uncommented (unless '#' is followed by a space) - thus this file defines the
|
||||||
|
# default key bindings.
|
||||||
|
|
||||||
|
# If this is enabled, treat all the following bindings as default.
|
||||||
|
#default-bindings start
|
||||||
|
|
||||||
|
#MBTN_LEFT ignore # don't do anything
|
||||||
|
#MBTN_LEFT_DBL cycle fullscreen # toggle fullscreen
|
||||||
|
#MBTN_RIGHT cycle pause # toggle pause/playback mode
|
||||||
|
#MBTN_BACK playlist-prev # skip to the previous file
|
||||||
|
#MBTN_FORWARD playlist-next # skip to the next file
|
||||||
|
|
||||||
|
# Mouse wheels, touchpad or other input devices that have axes
|
||||||
|
# if the input devices supports precise scrolling it will also scale the
|
||||||
|
# numeric value accordingly
|
||||||
|
#WHEEL_UP seek 10 # seek 10 seconds forward
|
||||||
|
#WHEEL_DOWN seek -10 # seek 10 seconds backward
|
||||||
|
#WHEEL_LEFT add volume -2
|
||||||
|
#WHEEL_RIGHT add volume 2
|
||||||
|
|
||||||
|
## Seek units are in seconds, but note that these are limited by keyframes
|
||||||
|
#RIGHT seek 5 # seek 5 seconds forward
|
||||||
|
#LEFT seek -5 # seek 5 seconds backward
|
||||||
|
#UP seek 60 # seek 1 minute forward
|
||||||
|
#DOWN seek -60 # seek 1 minute backward
|
||||||
|
# Do smaller, always exact (non-keyframe-limited), seeks with shift.
|
||||||
|
# Don't show them on the OSD (no-osd).
|
||||||
|
#Shift+RIGHT no-osd seek 1 exact # seek exactly 1 second forward
|
||||||
|
#Shift+LEFT no-osd seek -1 exact # seek exactly 1 second backward
|
||||||
|
#Shift+UP no-osd seek 5 exact # seek exactly 5 seconds forward
|
||||||
|
#Shift+DOWN no-osd seek -5 exact # seek exactly 5 seconds backward
|
||||||
|
#Ctrl+LEFT no-osd sub-seek -1 # seek to the previous subtitle
|
||||||
|
#Ctrl+RIGHT no-osd sub-seek 1 # seek to the next subtitle
|
||||||
|
#Ctrl+Shift+LEFT sub-step -1 # change subtitle timing such that the previous subtitle is displayed
|
||||||
|
#Ctrl+Shift+RIGHT sub-step 1 # change subtitle timing such that the next subtitle is displayed
|
||||||
|
#Alt+left add video-pan-x 0.1 # move the video right
|
||||||
|
#Alt+right add video-pan-x -0.1 # move the video left
|
||||||
|
#Alt+up add video-pan-y 0.1 # move the video down
|
||||||
|
#Alt+down add video-pan-y -0.1 # move the video up
|
||||||
|
#Alt++ add video-zoom 0.1 # zoom in
|
||||||
|
#Alt+- add video-zoom -0.1 # zoom out
|
||||||
|
#Alt+BS set video-zoom 0 ; set video-pan-x 0 ; set video-pan-y 0 # reset zoom and pan settings
|
||||||
|
#PGUP add chapter 1 # seek to the next chapter
|
||||||
|
#PGDWN add chapter -1 # seek to the previous chapter
|
||||||
|
#Shift+PGUP seek 600 # seek 10 minutes forward
|
||||||
|
#Shift+PGDWN seek -600 # seek 10 minutes backward
|
||||||
|
#[ multiply speed 1/1.1 # decrease the playback speed
|
||||||
|
#] multiply speed 1.1 # increase the playback speed
|
||||||
|
#{ multiply speed 0.5 # halve the playback speed
|
||||||
|
#} multiply speed 2.0 # double the playback speed
|
||||||
|
#BS set speed 1.0 # reset the speed to normal
|
||||||
|
#Shift+BS revert-seek # undo the previous (or marked) seek
|
||||||
|
#Shift+Ctrl+BS revert-seek mark # mark the position for revert-seek
|
||||||
|
#q quit
|
||||||
|
#Q quit-watch-later # exit and remember the playback position
|
||||||
|
#q {encode} quit 4
|
||||||
|
#ESC set fullscreen no # leave fullscreen
|
||||||
|
#ESC {encode} quit 4
|
||||||
|
#p cycle pause # toggle pause/playback mode
|
||||||
|
#. frame-step # advance one frame and pause
|
||||||
|
#, frame-back-step # go back by one frame and pause
|
||||||
|
#SPACE cycle pause # toggle pause/playback mode
|
||||||
|
#> playlist-next # skip to the next file
|
||||||
|
#ENTER playlist-next # skip to the next file
|
||||||
|
#< playlist-prev # skip to the previous file
|
||||||
|
#O no-osd cycle-values osd-level 3 1 # toggle displaying the OSD on user interaction or always
|
||||||
|
#o show-progress # show playback progress
|
||||||
|
#P show-progress # show playback progress
|
||||||
|
#i script-binding stats/display-stats # display information and statistics
|
||||||
|
#I script-binding stats/display-stats-toggle # toggle displaying information and statistics
|
||||||
|
#` script-binding console/enable # open the console
|
||||||
|
#z add sub-delay -0.1 # shift subtitles 100 ms earlier
|
||||||
|
#Z add sub-delay +0.1 # delay subtitles by 100 ms
|
||||||
|
#x add sub-delay +0.1 # delay subtitles by 100 ms
|
||||||
|
#ctrl++ add audio-delay 0.100 # change audio/video sync by delaying the audio
|
||||||
|
#ctrl+- add audio-delay -0.100 # change audio/video sync by shifting the audio earlier
|
||||||
|
#Shift+g add sub-scale +0.1 # increase the subtitle font size
|
||||||
|
#Shift+f add sub-scale -0.1 # decrease the subtitle font size
|
||||||
|
#9 add volume -2
|
||||||
|
#/ add volume -2
|
||||||
|
#0 add volume 2
|
||||||
|
#* add volume 2
|
||||||
|
#m cycle mute # toggle mute
|
||||||
|
#1 add contrast -1
|
||||||
|
#2 add contrast 1
|
||||||
|
#3 add brightness -1
|
||||||
|
#4 add brightness 1
|
||||||
|
#5 add gamma -1
|
||||||
|
#6 add gamma 1
|
||||||
|
#7 add saturation -1
|
||||||
|
#8 add saturation 1
|
||||||
|
#Alt+0 set current-window-scale 0.5 # halve the window size
|
||||||
|
#Alt+1 set current-window-scale 1.0 # reset the window size
|
||||||
|
#Alt+2 set current-window-scale 2.0 # double the window size
|
||||||
|
#d cycle deinterlace # toggle the deinterlacing filter
|
||||||
|
#r add sub-pos -1 # move subtitles up
|
||||||
|
#R add sub-pos +1 # move subtitles down
|
||||||
|
#t add sub-pos +1 # move subtitles down
|
||||||
|
#v cycle sub-visibility # hide or show the subtitles
|
||||||
|
#Alt+v cycle secondary-sub-visibility # hide or show the secondary subtitles
|
||||||
|
#V cycle sub-ass-vsfilter-aspect-compat # toggle stretching SSA/ASS subtitles with anamorphic videos to match the historical renderer
|
||||||
|
#u cycle-values sub-ass-override "force" "no" # toggle overriding SSA/ASS subtitle styles with the normal styles
|
||||||
|
#j cycle sub # switch subtitle track
|
||||||
|
#J cycle sub down # switch subtitle track backwards
|
||||||
|
#SHARP cycle audio # switch audio track
|
||||||
|
#_ cycle video # switch video track
|
||||||
|
#T cycle ontop # toggle placing the video on top of other windows
|
||||||
|
#f cycle fullscreen # toggle fullscreen
|
||||||
|
#s screenshot # take a screenshot of the video in its original resolution with subtitles
|
||||||
|
#S screenshot video # take a screenshot of the video in its original resolution without subtitles
|
||||||
|
#Ctrl+s screenshot window # take a screenshot of the window with OSD and subtitles
|
||||||
|
#Alt+s screenshot each-frame # automatically screenshot every frame; issue this command again to stop taking screenshots
|
||||||
|
#w add panscan -0.1 # decrease panscan
|
||||||
|
#W add panscan +0.1 # shrink black bars by cropping the video
|
||||||
|
#e add panscan +0.1 # shrink black bars by cropping the video
|
||||||
|
#A cycle-values video-aspect-override "16:9" "4:3" "2.35:1" "-1" # cycle the video aspect ratio ("-1" is the container aspect)
|
||||||
|
#POWER quit
|
||||||
|
#PLAY cycle pause # toggle pause/playback mode
|
||||||
|
#PAUSE cycle pause # toggle pause/playback mode
|
||||||
|
#PLAYPAUSE cycle pause # toggle pause/playback mode
|
||||||
|
#PLAYONLY set pause no # unpause
|
||||||
|
#PAUSEONLY set pause yes # pause
|
||||||
|
#STOP quit
|
||||||
|
#FORWARD seek 60 # seek 1 minute forward
|
||||||
|
#REWIND seek -60 # seek 1 minute backward
|
||||||
|
#NEXT playlist-next # skip to the next file
|
||||||
|
#PREV playlist-prev # skip to the previous file
|
||||||
|
#VOLUME_UP add volume 2
|
||||||
|
#VOLUME_DOWN add volume -2
|
||||||
|
#MUTE cycle mute # toggle mute
|
||||||
|
#CLOSE_WIN quit
|
||||||
|
#CLOSE_WIN {encode} quit 4
|
||||||
|
#ctrl+w quit
|
||||||
|
#E cycle edition # switch edition
|
||||||
|
#l ab-loop # set/clear A-B loop points
|
||||||
|
#L cycle-values loop-file "inf" "no" # toggle infinite looping
|
||||||
|
#ctrl+c quit 4
|
||||||
|
#DEL script-binding osc/visibility # cycle OSC visibility between never, auto (mouse-move) and always
|
||||||
|
#ctrl+h cycle-values hwdec "auto" "no" # toggle hardware decoding
|
||||||
|
#F8 show-text ${playlist} # show the playlist
|
||||||
|
#F9 show-text ${track-list} # show the list of video, audio and sub tracks
|
||||||
|
|
||||||
|
#
|
||||||
|
# Legacy bindings (may or may not be removed in the future)
|
||||||
|
#
|
||||||
|
#! add chapter -1 # seek to the previous chapter
|
||||||
|
#@ add chapter 1 # seek to the next chapter
|
||||||
|
|
||||||
|
#
|
||||||
|
# Not assigned by default
|
||||||
|
# (not an exhaustive list of unbound commands)
|
||||||
|
#
|
||||||
|
|
||||||
|
# ? cycle sub-forced-only # toggle DVD forced subs
|
||||||
|
# ? stop # stop playback (quit or enter idle mode)
|
||||||
|
|
||||||
|
|
||||||
|
### Shaders ###
|
||||||
|
|
||||||
|
CTRL+1 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl:~~/shaders/Anime4K_Restore_CNN_VL.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_VL.glsl:~~/shaders/Anime4K_AutoDownscalePre_x2.glsl:~~/shaders/Anime4K_AutoDownscalePre_x4.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode A (HQ)"
|
||||||
|
CTRL+2 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl:~~/shaders/Anime4K_Restore_CNN_Soft_VL.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_VL.glsl:~~/shaders/Anime4K_AutoDownscalePre_x2.glsl:~~/shaders/Anime4K_AutoDownscalePre_x4.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode B (HQ)"
|
||||||
|
CTRL+3 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl:~~/shaders/Anime4K_Upscale_Denoise_CNN_x2_VL.glsl:~~/shaders/Anime4K_AutoDownscalePre_x2.glsl:~~/shaders/Anime4K_AutoDownscalePre_x4.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode C (HQ)"
|
||||||
|
CTRL+4 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl:~~/shaders/Anime4K_Restore_CNN_VL.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_VL.glsl:~~/shaders/Anime4K_Restore_CNN_M.glsl:~~/shaders/Anime4K_AutoDownscalePre_x2.glsl:~~/shaders/Anime4K_AutoDownscalePre_x4.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode A+A (HQ)"
|
||||||
|
CTRL+5 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl:~~/shaders/Anime4K_Restore_CNN_Soft_VL.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_VL.glsl:~~/shaders/Anime4K_AutoDownscalePre_x2.glsl:~~/shaders/Anime4K_AutoDownscalePre_x4.glsl:~~/shaders/Anime4K_Restore_CNN_Soft_M.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode B+B (HQ)"
|
||||||
|
CTRL+6 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl:~~/shaders/Anime4K_Upscale_Denoise_CNN_x2_VL.glsl:~~/shaders/Anime4K_AutoDownscalePre_x2.glsl:~~/shaders/Anime4K_AutoDownscalePre_x4.glsl:~~/shaders/Anime4K_Restore_CNN_M.glsl:~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode C+A (HQ)"
|
||||||
|
|
||||||
|
CTRL+0 no-osd change-list glsl-shaders clr ""; show-text "GLSL shaders cleared"
|
||||||
93
config/mpv/mplayer-input.conf
Normal file
93
config/mpv/mplayer-input.conf
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
##
|
||||||
|
## MPlayer-style key bindings
|
||||||
|
##
|
||||||
|
## Save it as ~/.config/mpv/input.conf to use it.
|
||||||
|
##
|
||||||
|
## Generally, it's recommended to use this as reference-only.
|
||||||
|
##
|
||||||
|
|
||||||
|
RIGHT seek +10
|
||||||
|
LEFT seek -10
|
||||||
|
DOWN seek -60
|
||||||
|
UP seek +60
|
||||||
|
PGUP seek 600
|
||||||
|
PGDWN seek -600
|
||||||
|
m cycle mute
|
||||||
|
SHARP cycle audio # switch audio streams
|
||||||
|
+ add audio-delay 0.100
|
||||||
|
= add audio-delay 0.100
|
||||||
|
- add audio-delay -0.100
|
||||||
|
[ multiply speed 0.9091 # scale playback speed
|
||||||
|
] multiply speed 1.1
|
||||||
|
{ multiply speed 0.5
|
||||||
|
} multiply speed 2.0
|
||||||
|
BS set speed 1.0 # reset speed to normal
|
||||||
|
q quit
|
||||||
|
ESC quit
|
||||||
|
ENTER playlist-next force # skip to next file
|
||||||
|
p cycle pause
|
||||||
|
. frame-step # advance one frame and pause
|
||||||
|
SPACE cycle pause
|
||||||
|
HOME set playlist-pos 0 # not the same as MPlayer
|
||||||
|
#END pt_up_step -1
|
||||||
|
> playlist-next # skip to next file
|
||||||
|
< playlist-prev # previous
|
||||||
|
#INS alt_src_step 1
|
||||||
|
#DEL alt_src_step -1
|
||||||
|
o osd
|
||||||
|
I show-text "${filename}" # display filename in osd
|
||||||
|
P show-progress
|
||||||
|
z add sub-delay -0.1 # subtract 100 ms delay from subs
|
||||||
|
x add sub-delay +0.1 # add
|
||||||
|
9 add volume -1
|
||||||
|
/ add volume -1
|
||||||
|
0 add volume 1
|
||||||
|
* add volume 1
|
||||||
|
1 add contrast -1
|
||||||
|
2 add contrast 1
|
||||||
|
3 add brightness -1
|
||||||
|
4 add brightness 1
|
||||||
|
5 add hue -1
|
||||||
|
6 add hue 1
|
||||||
|
7 add saturation -1
|
||||||
|
8 add saturation 1
|
||||||
|
( add balance -0.1 # adjust audio balance in favor of left
|
||||||
|
) add balance +0.1 # right
|
||||||
|
d cycle framedrop
|
||||||
|
D cycle deinterlace # toggle deinterlacer (auto-inserted filter)
|
||||||
|
r add sub-pos -1 # move subtitles up
|
||||||
|
t add sub-pos +1 # down
|
||||||
|
#? sub-step +1 # immediately display next subtitle
|
||||||
|
#? sub-step -1 # previous
|
||||||
|
#? add sub-scale +0.1 # increase subtitle font size
|
||||||
|
#? add sub-scale -0.1 # decrease subtitle font size
|
||||||
|
f cycle fullscreen
|
||||||
|
T cycle ontop # toggle video window ontop of other windows
|
||||||
|
w add panscan -0.1 # zoom out with -panscan 0 -fs
|
||||||
|
e add panscan +0.1 # in
|
||||||
|
c cycle stream-capture # save (and append) file/stream to stream.dump with -capture
|
||||||
|
s screenshot # take a screenshot (if you want PNG, use "--screenshot-format=png")
|
||||||
|
S screenshot - each-frame # S will take a png screenshot of every frame
|
||||||
|
|
||||||
|
h cycle tv-channel 1
|
||||||
|
l cycle tv-channel -1
|
||||||
|
n cycle tv-norm
|
||||||
|
#b tv_step_chanlist
|
||||||
|
|
||||||
|
#? add chapter -1 # skip to previous dvd chapter
|
||||||
|
#? add chapter +1 # next
|
||||||
|
|
||||||
|
##
|
||||||
|
## Advanced seek
|
||||||
|
## Uncomment the following lines to be able to seek to n% of the media with
|
||||||
|
## the Fx keys.
|
||||||
|
##
|
||||||
|
#F1 seek 10 absolute-percent
|
||||||
|
#F2 seek 20 absolute-percent
|
||||||
|
#F3 seek 30 absolute-percent
|
||||||
|
#F4 seek 40 absolute-percent
|
||||||
|
#F5 seek 50 absolute-percent
|
||||||
|
#F6 seek 60 absolute-percent
|
||||||
|
#F7 seek 70 absolute-percent
|
||||||
|
#F8 seek 80 absolute-percent
|
||||||
|
#F9 seek 90 absolute-percent
|
||||||
150
config/mpv/mpv.conf
Normal file
150
config/mpv/mpv.conf
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
#
|
||||||
|
# Example mpv configuration file
|
||||||
|
#
|
||||||
|
# Warning:
|
||||||
|
#
|
||||||
|
# The commented example options usually do _not_ set the default values. Call
|
||||||
|
# mpv with --list-options to see the default values for most options. There is
|
||||||
|
# no builtin or example mpv.conf with all the defaults.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Configuration files are read system-wide from /usr/local/etc/mpv.conf
|
||||||
|
# and per-user from ~/.config/mpv/mpv.conf, where per-user settings override
|
||||||
|
# system-wide settings, all of which are overridden by the command line.
|
||||||
|
#
|
||||||
|
# Configuration file settings and the command line options use the same
|
||||||
|
# underlying mechanisms. Most options can be put into the configuration file
|
||||||
|
# by dropping the preceding '--'. See the man page for a complete list of
|
||||||
|
# options.
|
||||||
|
#
|
||||||
|
# Lines starting with '#' are comments and are ignored.
|
||||||
|
#
|
||||||
|
# See the CONFIGURATION FILES section in the man page
|
||||||
|
# for a detailed description of the syntax.
|
||||||
|
#
|
||||||
|
# Profiles should be placed at the bottom of the configuration file to ensure
|
||||||
|
# that settings wanted as defaults are not restricted to specific profiles.
|
||||||
|
|
||||||
|
##################
|
||||||
|
# video settings #
|
||||||
|
##################
|
||||||
|
|
||||||
|
# Start in fullscreen mode by default.
|
||||||
|
#fs=yes
|
||||||
|
|
||||||
|
# force starting with centered window
|
||||||
|
#geometry=50%:50%
|
||||||
|
|
||||||
|
# don't allow a new window to have a size larger than 90% of the screen size
|
||||||
|
#autofit-larger=90%x90%
|
||||||
|
|
||||||
|
# Do not close the window on exit.
|
||||||
|
#keep-open=yes
|
||||||
|
|
||||||
|
# Do not wait with showing the video window until it has loaded. (This will
|
||||||
|
# resize the window once video is loaded. Also always shows a window with
|
||||||
|
# audio.)
|
||||||
|
#force-window=immediate
|
||||||
|
|
||||||
|
# Disable the On Screen Controller (OSC).
|
||||||
|
osc=no
|
||||||
|
|
||||||
|
# Keep the player window on top of all other windows.
|
||||||
|
#ontop=yes
|
||||||
|
|
||||||
|
# Specify high quality video rendering preset (for --vo=gpu only)
|
||||||
|
# Can cause performance problems with some drivers and GPUs.
|
||||||
|
profile=gpu-hq
|
||||||
|
# scale=ewa_lanczossharp
|
||||||
|
# cscale=ewa_lanczossharp
|
||||||
|
|
||||||
|
|
||||||
|
# Force video to lock on the display's refresh rate, and change video and audio
|
||||||
|
# speed to some degree to ensure synchronous playback - can cause problems
|
||||||
|
# with some drivers and desktop environments.
|
||||||
|
# video-sync=display-resample
|
||||||
|
# interpolation
|
||||||
|
# tscale=oversample
|
||||||
|
|
||||||
|
# Enable hardware decoding if available. Often, this does not work with all
|
||||||
|
# video outputs, but should work well with default settings on most systems.
|
||||||
|
# If performance or energy usage is an issue, forcing the vdpau or vaapi VOs
|
||||||
|
# may or may not help.
|
||||||
|
#hwdec=auto
|
||||||
|
|
||||||
|
##################
|
||||||
|
# audio settings #
|
||||||
|
##################
|
||||||
|
|
||||||
|
# Specify default audio device. You can list devices with: --audio-device=help
|
||||||
|
# The option takes the device string (the stuff between the '...').
|
||||||
|
#audio-device=alsa/default
|
||||||
|
|
||||||
|
# Do not filter audio to keep pitch when changing playback speed.
|
||||||
|
#audio-pitch-correction=no
|
||||||
|
|
||||||
|
# Output 5.1 audio natively, and upmix/downmix audio with a different format.
|
||||||
|
#audio-channels=5.1
|
||||||
|
# Disable any automatic remix, _if_ the audio output accepts the audio format.
|
||||||
|
# of the currently played file. See caveats mentioned in the manpage.
|
||||||
|
# (The default is "auto-safe", see manpage.)
|
||||||
|
#audio-channels=auto
|
||||||
|
|
||||||
|
##################
|
||||||
|
# other settings #
|
||||||
|
##################
|
||||||
|
|
||||||
|
# Pretend to be a web browser. Might fix playback with some streaming sites,
|
||||||
|
# but also will break with shoutcast streams.
|
||||||
|
#user-agent="Mozilla/5.0"
|
||||||
|
|
||||||
|
# cache settings
|
||||||
|
#
|
||||||
|
# Use a large seekable RAM cache even for local input.
|
||||||
|
#cache=yes
|
||||||
|
#
|
||||||
|
# Use extra large RAM cache (needs cache=yes to make it useful).
|
||||||
|
#demuxer-max-bytes=500M
|
||||||
|
#demuxer-max-back-bytes=100M
|
||||||
|
#
|
||||||
|
# Disable the behavior that the player will pause if the cache goes below a
|
||||||
|
# certain fill size.
|
||||||
|
#cache-pause=no
|
||||||
|
#
|
||||||
|
# Store cache payload on the hard disk instead of in RAM. (This may negatively
|
||||||
|
# impact performance unless used for slow input such as network.)
|
||||||
|
#cache-dir=~/.cache/
|
||||||
|
#cache-on-disk=yes
|
||||||
|
|
||||||
|
# Display English subtitles if available.
|
||||||
|
#slang=en
|
||||||
|
|
||||||
|
# Play Finnish audio if available, fall back to English otherwise.
|
||||||
|
#alang=fi,en
|
||||||
|
|
||||||
|
# Change subtitle encoding. For Arabic subtitles use 'cp1256'.
|
||||||
|
# If the file seems to be valid UTF-8, prefer UTF-8.
|
||||||
|
# (You can add '+' in front of the codepage to force it.)
|
||||||
|
#sub-codepage=cp1256
|
||||||
|
|
||||||
|
# You can also include other configuration files.
|
||||||
|
#include=/path/to/the/file/you/want/to/include
|
||||||
|
|
||||||
|
############
|
||||||
|
# Profiles #
|
||||||
|
############
|
||||||
|
|
||||||
|
# The options declared as part of profiles override global default settings,
|
||||||
|
# but only take effect when the profile is active.
|
||||||
|
|
||||||
|
# The following profile can be enabled on the command line with: --profile=eye-cancer
|
||||||
|
|
||||||
|
#[eye-cancer]
|
||||||
|
#sharpen=5
|
||||||
|
|
||||||
|
[twitch]
|
||||||
|
profile-cond=get("path", ""):find("^https://www.twitch.tv/") ~= nil
|
||||||
|
profile-restore=copy-equal
|
||||||
|
sub-font-size=30
|
||||||
|
sub-align-x=right
|
||||||
|
sub-align-y=top
|
||||||
61
config/mpv/restore-old-bindings.conf
Normal file
61
config/mpv/restore-old-bindings.conf
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
|
||||||
|
# This file contains all bindings that were removed after a certain release.
|
||||||
|
# If you want MPlayer bindings, use mplayer-input.conf
|
||||||
|
|
||||||
|
# Pick the bindings you want back and add them to your own input.conf. Append
|
||||||
|
# this file to your input.conf if you want them all back:
|
||||||
|
#
|
||||||
|
# cat restore-old-bindings.conf >> ~/.config/mpv/input.conf
|
||||||
|
#
|
||||||
|
# Older installations use ~/.mpv/input.conf instead.
|
||||||
|
|
||||||
|
# changed in mpv 0.27.0 (macOS and Wayland only)
|
||||||
|
|
||||||
|
# WHEEL_UP seek 10
|
||||||
|
# WHEEL_DOWN seek -10
|
||||||
|
# WHEEL_LEFT seek 5
|
||||||
|
# WHEEL_RIGHT seek -5
|
||||||
|
|
||||||
|
# changed in mpv 0.26.0
|
||||||
|
|
||||||
|
h cycle tv-channel -1 # previous channel
|
||||||
|
k cycle tv-channel +1 # next channel
|
||||||
|
H cycle dvb-channel-name -1 # previous channel
|
||||||
|
K cycle dvb-channel-name +1 # next channel
|
||||||
|
|
||||||
|
I show-text "${filename}" # display filename in osd
|
||||||
|
|
||||||
|
# changed in mpv 0.24.0
|
||||||
|
|
||||||
|
L cycle-values loop "inf" "no"
|
||||||
|
|
||||||
|
# changed in mpv 0.10.0
|
||||||
|
|
||||||
|
O osd
|
||||||
|
D cycle deinterlace
|
||||||
|
d cycle framedrop
|
||||||
|
|
||||||
|
# changed in mpv 0.7.0
|
||||||
|
|
||||||
|
ENTER playlist-next force
|
||||||
|
|
||||||
|
# changed in mpv 0.6.0
|
||||||
|
|
||||||
|
ESC quit
|
||||||
|
|
||||||
|
# changed in mpv 0.5.0
|
||||||
|
|
||||||
|
PGUP seek 600
|
||||||
|
PGDWN seek -600
|
||||||
|
RIGHT seek 10
|
||||||
|
LEFT seek -10
|
||||||
|
+ add audio-delay 0.100
|
||||||
|
- add audio-delay -0.100
|
||||||
|
( add balance -0.1
|
||||||
|
) add balance 0.1
|
||||||
|
F cycle sub-forced-only
|
||||||
|
TAB cycle program
|
||||||
|
A cycle angle
|
||||||
|
U stop
|
||||||
|
o osd
|
||||||
|
I show-text "${filename}"
|
||||||
70
config/mpv/script-opts/mpv_thumbnail_script.conf
Normal file
70
config/mpv/script-opts/mpv_thumbnail_script.conf
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# The thumbnail cache directory.
|
||||||
|
# On Windows this defaults to %TEMP%\mpv_thumbs_cache,
|
||||||
|
# and on other platforms to /tmp/mpv_thumbs_cache.
|
||||||
|
# The directory will be created automatically, but must be writeable!
|
||||||
|
# Use absolute paths, and take note that environment variables like %TEMP% are unsupported (despite the default)!
|
||||||
|
cache_directory=/tmp/my_mpv_thumbnails
|
||||||
|
# THIS IS NOT A WINDOWS PATH. COMMENT IT OUT OR ADJUST IT YOURSELF.
|
||||||
|
|
||||||
|
# Whether to generate thumbnails automatically on video load, without a keypress
|
||||||
|
# Defaults to yes
|
||||||
|
autogenerate=yes
|
||||||
|
|
||||||
|
# Only automatically thumbnail videos shorter than this (in seconds)
|
||||||
|
# You will have to press T (or your own keybind) to enable the thumbnail previews
|
||||||
|
# Set to 0 to disable the check, ie. thumbnail videos no matter how long they are
|
||||||
|
# Defaults to 3600 (one hour)
|
||||||
|
# autogenerate_max_duration=3600
|
||||||
|
|
||||||
|
# Use mpv to generate thumbnail even if ffmpeg is found in PATH
|
||||||
|
# ffmpeg is slightly faster than mpv but lacks support for ordered chapters in MKVs,
|
||||||
|
# which can break the resulting thumbnails. You have been warned.
|
||||||
|
# Defaults to yes (don't use ffmpeg)
|
||||||
|
# prefer_mpv=[yes/no]
|
||||||
|
|
||||||
|
# Explicitly disable subtitles on the mpv sub-calls
|
||||||
|
# mpv can and will by default render subtitles into the thumbnails.
|
||||||
|
# If this is not what you wish, set mpv_no_sub to yes
|
||||||
|
# Defaults to no
|
||||||
|
# mpv_no_sub=[yes/no]
|
||||||
|
|
||||||
|
# Enable to disable the built-in keybind ("T") to add your own, see after the block
|
||||||
|
# disable_keybinds=[yes/no]
|
||||||
|
|
||||||
|
# The maximum dimensions of the thumbnails, in pixels
|
||||||
|
# Defaults to 200 and 200
|
||||||
|
# thumbnail_width=200
|
||||||
|
# thumbnail_height=200
|
||||||
|
|
||||||
|
# The thumbnail count target
|
||||||
|
# (This will result in a thumbnail every ~10 seconds for a 25 minute video)
|
||||||
|
# thumbnail_count=150
|
||||||
|
|
||||||
|
# The above target count will be adjusted by the minimum and
|
||||||
|
# maximum time difference between thumbnails.
|
||||||
|
# The thumbnail_count will be used to calculate a target separation,
|
||||||
|
# and min/max_delta will be used to constrict it.
|
||||||
|
|
||||||
|
# In other words, thumbnails will be:
|
||||||
|
# - at least min_delta seconds apart (limiting the amount)
|
||||||
|
# - at most max_delta seconds apart (raising the amount if needed)
|
||||||
|
# Defaults to 5 and 90, values are seconds
|
||||||
|
# min_delta=5
|
||||||
|
# max_delta=90
|
||||||
|
# 120 seconds aka 2 minutes will add more thumbnails only when the video is over 5 hours long!
|
||||||
|
|
||||||
|
# Below are overrides for remote urls (you generally want less thumbnails, because it's slow!)
|
||||||
|
# Thumbnailing network paths will be done with mpv (leveraging youtube-dl)
|
||||||
|
|
||||||
|
# Allow thumbnailing network paths (naive check for "://")
|
||||||
|
# Defaults to no
|
||||||
|
# thumbnail_network=[yes/no]
|
||||||
|
# Override thumbnail count, min/max delta, as above
|
||||||
|
# remote_thumbnail_count=60
|
||||||
|
# remote_min_delta=15
|
||||||
|
# remote_max_delta=120
|
||||||
|
|
||||||
|
# Try to grab the raw stream and disable ytdl for the mpv subcalls
|
||||||
|
# Much faster than passing the url to ytdl again, but may cause problems with some sites
|
||||||
|
# Defaults to yes
|
||||||
|
# remote_direct_stream=[yes/no]
|
||||||
19
config/mpv/scripts/mpv-i3-floating-centered.lua
Normal file
19
config/mpv/scripts/mpv-i3-floating-centered.lua
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
|
||||||
|
local pid = utils.getpid()
|
||||||
|
|
||||||
|
local function center_floating_mpv()
|
||||||
|
mpv_window_id = io.popen("xdotool search --pid " .. pid):read()
|
||||||
|
|
||||||
|
-- mpv can have a slight delay in launching a window, or be called without one at all
|
||||||
|
if mpv_window_id == nil then return end
|
||||||
|
|
||||||
|
floating = io.popen("xprop -id " .. mpv_window_id):read("*a")
|
||||||
|
if string.match(floating, "FLOATING") then
|
||||||
|
os.execute("i3-msg -q '[id=" .. mpv_window_id .. "]' move position center")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mp.register_event("playback-restart", center_floating_mpv)
|
||||||
|
-- mpv should still be centered when fullscreen is toggled
|
||||||
|
mp.observe_property("fullscreen", "bool", center_floating_mpv)
|
||||||
4336
config/mpv/scripts/mpv_thumbnail_script_client_osc.lua
Normal file
4336
config/mpv/scripts/mpv_thumbnail_script_client_osc.lua
Normal file
File diff suppressed because it is too large
Load diff
736
config/mpv/scripts/mpv_thumbnail_script_server-1.lua
Normal file
736
config/mpv/scripts/mpv_thumbnail_script_server-1.lua
Normal file
|
|
@ -0,0 +1,736 @@
|
||||||
|
--[[
|
||||||
|
Copyright (C) 2017 AMM
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
]]--
|
||||||
|
--[[
|
||||||
|
mpv_thumbnail_script.lua 0.4.7 - commit 6282073 (branch master)
|
||||||
|
https://github.com/TheAMM/mpv_thumbnail_script
|
||||||
|
Built on 2022-02-05 16:00:24
|
||||||
|
]]--
|
||||||
|
local assdraw = require 'mp.assdraw'
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local opt = require 'mp.options'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
|
||||||
|
-- Determine platform --
|
||||||
|
ON_WINDOWS = (package.config:sub(1,1) ~= '/')
|
||||||
|
|
||||||
|
-- Some helper functions needed to parse the options --
|
||||||
|
function isempty(v) return (v == false) or (v == nil) or (v == "") or (v == 0) or (type(v) == "table" and next(v) == nil) end
|
||||||
|
|
||||||
|
function divmod (a, b)
|
||||||
|
return math.floor(a / b), a % b
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Better modulo
|
||||||
|
function bmod( i, N )
|
||||||
|
return (i % N + N) % N
|
||||||
|
end
|
||||||
|
|
||||||
|
function join_paths(...)
|
||||||
|
local sep = ON_WINDOWS and "\\" or "/"
|
||||||
|
local result = "";
|
||||||
|
for i, p in pairs({...}) do
|
||||||
|
if p ~= "" then
|
||||||
|
if is_absolute_path(p) then
|
||||||
|
result = p
|
||||||
|
else
|
||||||
|
result = (result ~= "") and (result:gsub("[\\"..sep.."]*$", "") .. sep .. p) or p
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result:gsub("[\\"..sep.."]*$", "")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- /some/path/file.ext -> /some/path, file.ext
|
||||||
|
function split_path( path )
|
||||||
|
local sep = ON_WINDOWS and "\\" or "/"
|
||||||
|
local first_index, last_index = path:find('^.*' .. sep)
|
||||||
|
|
||||||
|
if last_index == nil then
|
||||||
|
return "", path
|
||||||
|
else
|
||||||
|
local dir = path:sub(0, last_index-1)
|
||||||
|
local file = path:sub(last_index+1, -1)
|
||||||
|
|
||||||
|
return dir, file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function is_absolute_path( path )
|
||||||
|
local tmp, is_win = path:gsub("^[A-Z]:\\", "")
|
||||||
|
local tmp, is_unix = path:gsub("^/", "")
|
||||||
|
return (is_win > 0) or (is_unix > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Set(source)
|
||||||
|
local set = {}
|
||||||
|
for _, l in ipairs(source) do set[l] = true end
|
||||||
|
return set
|
||||||
|
end
|
||||||
|
|
||||||
|
---------------------------
|
||||||
|
-- More helper functions --
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
-- Removes all keys from a table, without destroying the reference to it
|
||||||
|
function clear_table(target)
|
||||||
|
for key, value in pairs(target) do
|
||||||
|
target[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function shallow_copy(target)
|
||||||
|
local copy = {}
|
||||||
|
for k, v in pairs(target) do
|
||||||
|
copy[k] = v
|
||||||
|
end
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rounds to given decimals. eg. round_dec(3.145, 0) => 3
|
||||||
|
function round_dec(num, idp)
|
||||||
|
local mult = 10^(idp or 0)
|
||||||
|
return math.floor(num * mult + 0.5) / mult
|
||||||
|
end
|
||||||
|
|
||||||
|
function file_exists(name)
|
||||||
|
local f = io.open(name, "rb")
|
||||||
|
if f ~= nil then
|
||||||
|
local ok, err, code = f:read(1)
|
||||||
|
io.close(f)
|
||||||
|
return code == nil
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function path_exists(name)
|
||||||
|
local f = io.open(name, "rb")
|
||||||
|
if f ~= nil then
|
||||||
|
io.close(f)
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function create_directories(path)
|
||||||
|
local cmd
|
||||||
|
if ON_WINDOWS then
|
||||||
|
cmd = { args = {"cmd", "/c", "mkdir", path} }
|
||||||
|
else
|
||||||
|
cmd = { args = {"mkdir", "-p", path} }
|
||||||
|
end
|
||||||
|
utils.subprocess(cmd)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find an executable in PATH or CWD with the given name
|
||||||
|
function find_executable(name)
|
||||||
|
local delim = ON_WINDOWS and ";" or ":"
|
||||||
|
|
||||||
|
local pwd = os.getenv("PWD") or utils.getcwd()
|
||||||
|
local path = os.getenv("PATH")
|
||||||
|
|
||||||
|
local env_path = pwd .. delim .. path -- Check CWD first
|
||||||
|
|
||||||
|
local result, filename
|
||||||
|
for path_dir in env_path:gmatch("[^"..delim.."]+") do
|
||||||
|
filename = join_paths(path_dir, name)
|
||||||
|
if file_exists(filename) then
|
||||||
|
result = filename
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local ExecutableFinder = { path_cache = {} }
|
||||||
|
-- Searches for an executable and caches the result if any
|
||||||
|
function ExecutableFinder:get_executable_path( name, raw_name )
|
||||||
|
name = ON_WINDOWS and not raw_name and (name .. ".exe") or name
|
||||||
|
|
||||||
|
if self.path_cache[name] == nil then
|
||||||
|
self.path_cache[name] = find_executable(name) or false
|
||||||
|
end
|
||||||
|
return self.path_cache[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Format seconds to HH.MM.SS.sss
|
||||||
|
function format_time(seconds, sep, decimals)
|
||||||
|
decimals = decimals == nil and 3 or decimals
|
||||||
|
sep = sep and sep or "."
|
||||||
|
local s = seconds
|
||||||
|
local h, s = divmod(s, 60*60)
|
||||||
|
local m, s = divmod(s, 60)
|
||||||
|
|
||||||
|
local second_format = string.format("%%0%d.%df", 2+(decimals > 0 and decimals+1 or 0), decimals)
|
||||||
|
|
||||||
|
return string.format("%02d"..sep.."%02d"..sep..second_format, h, m, s)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Format seconds to 1h 2m 3.4s
|
||||||
|
function format_time_hms(seconds, sep, decimals, force_full)
|
||||||
|
decimals = decimals == nil and 1 or decimals
|
||||||
|
sep = sep ~= nil and sep or " "
|
||||||
|
|
||||||
|
local s = seconds
|
||||||
|
local h, s = divmod(s, 60*60)
|
||||||
|
local m, s = divmod(s, 60)
|
||||||
|
|
||||||
|
if force_full or h > 0 then
|
||||||
|
return string.format("%dh"..sep.."%dm"..sep.."%." .. tostring(decimals) .. "fs", h, m, s)
|
||||||
|
elseif m > 0 then
|
||||||
|
return string.format("%dm"..sep.."%." .. tostring(decimals) .. "fs", m, s)
|
||||||
|
else
|
||||||
|
return string.format("%." .. tostring(decimals) .. "fs", s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Writes text on OSD and console
|
||||||
|
function log_info(txt, timeout)
|
||||||
|
timeout = timeout or 1.5
|
||||||
|
msg.info(txt)
|
||||||
|
mp.osd_message(txt, timeout)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Join table items, ala ({"a", "b", "c"}, "=", "-", ", ") => "=a-, =b-, =c-"
|
||||||
|
function join_table(source, before, after, sep)
|
||||||
|
before = before or ""
|
||||||
|
after = after or ""
|
||||||
|
sep = sep or ", "
|
||||||
|
local result = ""
|
||||||
|
for i, v in pairs(source) do
|
||||||
|
if not isempty(v) then
|
||||||
|
local part = before .. v .. after
|
||||||
|
if i == 1 then
|
||||||
|
result = part
|
||||||
|
else
|
||||||
|
result = result .. sep .. part
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function wrap(s, char)
|
||||||
|
char = char or "'"
|
||||||
|
return char .. s .. char
|
||||||
|
end
|
||||||
|
-- Wraps given string into 'string' and escapes any 's in it
|
||||||
|
function escape_and_wrap(s, char, replacement)
|
||||||
|
char = char or "'"
|
||||||
|
replacement = replacement or "\\" .. char
|
||||||
|
return wrap(string.gsub(s, char, replacement), char)
|
||||||
|
end
|
||||||
|
-- Escapes single quotes in a string and wraps the input in single quotes
|
||||||
|
function escape_single_bash(s)
|
||||||
|
return escape_and_wrap(s, "'", "'\\''")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns (a .. b) if b is not empty or nil
|
||||||
|
function joined_or_nil(a, b)
|
||||||
|
return not isempty(b) and (a .. b) or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Put items from one table into another
|
||||||
|
function extend_table(target, source)
|
||||||
|
for i, v in pairs(source) do
|
||||||
|
table.insert(target, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Creates a handle and filename for a temporary random file (in current directory)
|
||||||
|
function create_temporary_file(base, mode, suffix)
|
||||||
|
local handle, filename
|
||||||
|
suffix = suffix or ""
|
||||||
|
while true do
|
||||||
|
filename = base .. tostring(math.random(1, 5000)) .. suffix
|
||||||
|
handle = io.open(filename, "r")
|
||||||
|
if not handle then
|
||||||
|
handle = io.open(filename, mode)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
io.close(handle)
|
||||||
|
end
|
||||||
|
return handle, filename
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function get_processor_count()
|
||||||
|
local proc_count
|
||||||
|
|
||||||
|
if ON_WINDOWS then
|
||||||
|
proc_count = tonumber(os.getenv("NUMBER_OF_PROCESSORS"))
|
||||||
|
else
|
||||||
|
local cpuinfo_handle = io.open("/proc/cpuinfo")
|
||||||
|
if cpuinfo_handle ~= nil then
|
||||||
|
local cpuinfo_contents = cpuinfo_handle:read("*a")
|
||||||
|
local _, replace_count = cpuinfo_contents:gsub('processor', '')
|
||||||
|
proc_count = replace_count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if proc_count and proc_count > 0 then
|
||||||
|
return proc_count
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function substitute_values(string, values)
|
||||||
|
local substitutor = function(match)
|
||||||
|
if match == "%" then
|
||||||
|
return "%"
|
||||||
|
else
|
||||||
|
-- nil is discarded by gsub
|
||||||
|
return values[match]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local substituted = string:gsub('%%(.)', substitutor)
|
||||||
|
return substituted
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ASS HELPERS --
|
||||||
|
function round_rect_top( ass, x0, y0, x1, y1, r )
|
||||||
|
local c = 0.551915024494 * r -- circle approximation
|
||||||
|
ass:move_to(x0 + r, y0)
|
||||||
|
ass:line_to(x1 - r, y0) -- top line
|
||||||
|
if r > 0 then
|
||||||
|
ass:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r) -- top right corner
|
||||||
|
end
|
||||||
|
ass:line_to(x1, y1) -- right line
|
||||||
|
ass:line_to(x0, y1) -- bottom line
|
||||||
|
ass:line_to(x0, y0 + r) -- left line
|
||||||
|
if r > 0 then
|
||||||
|
ass:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0) -- top left corner
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function round_rect(ass, x0, y0, x1, y1, rtl, rtr, rbr, rbl)
|
||||||
|
local c = 0.551915024494
|
||||||
|
ass:move_to(x0 + rtl, y0)
|
||||||
|
ass:line_to(x1 - rtr, y0) -- top line
|
||||||
|
if rtr > 0 then
|
||||||
|
ass:bezier_curve(x1 - rtr + rtr*c, y0, x1, y0 + rtr - rtr*c, x1, y0 + rtr) -- top right corner
|
||||||
|
end
|
||||||
|
ass:line_to(x1, y1 - rbr) -- right line
|
||||||
|
if rbr > 0 then
|
||||||
|
ass:bezier_curve(x1, y1 - rbr + rbr*c, x1 - rbr + rbr*c, y1, x1 - rbr, y1) -- bottom right corner
|
||||||
|
end
|
||||||
|
ass:line_to(x0 + rbl, y1) -- bottom line
|
||||||
|
if rbl > 0 then
|
||||||
|
ass:bezier_curve(x0 + rbl - rbl*c, y1, x0, y1 - rbl + rbl*c, x0, y1 - rbl) -- bottom left corner
|
||||||
|
end
|
||||||
|
ass:line_to(x0, y0 + rtl) -- left line
|
||||||
|
if rtl > 0 then
|
||||||
|
ass:bezier_curve(x0, y0 + rtl - rtl*c, x0 + rtl - rtl*c, y0, x0 + rtl, y0) -- top left corner
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local SCRIPT_NAME = "mpv_thumbnail_script"
|
||||||
|
|
||||||
|
local default_cache_base = ON_WINDOWS and os.getenv("TEMP") or "/tmp/"
|
||||||
|
|
||||||
|
local thumbnailer_options = {
|
||||||
|
-- The thumbnail directory
|
||||||
|
cache_directory = join_paths(default_cache_base, "mpv_thumbs_cache"),
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
-- Generation options --
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
-- Automatically generate the thumbnails on video load, without a keypress
|
||||||
|
autogenerate = true,
|
||||||
|
|
||||||
|
-- Only automatically thumbnail videos shorter than this (seconds)
|
||||||
|
autogenerate_max_duration = 3600, -- 1 hour
|
||||||
|
|
||||||
|
-- SHA1-sum filenames over this length
|
||||||
|
-- It's nice to know what files the thumbnails are (hence directory names)
|
||||||
|
-- but long URLs may approach filesystem limits.
|
||||||
|
hash_filename_length = 128,
|
||||||
|
|
||||||
|
-- Use mpv to generate thumbnail even if ffmpeg is found in PATH
|
||||||
|
-- ffmpeg does not handle ordered chapters (MKVs which rely on other MKVs)!
|
||||||
|
-- mpv is a bit slower, but has better support overall (eg. subtitles in the previews)
|
||||||
|
prefer_mpv = true,
|
||||||
|
|
||||||
|
-- Explicitly disable subtitles on the mpv sub-calls
|
||||||
|
mpv_no_sub = false,
|
||||||
|
-- Add a "--no-config" to the mpv sub-call arguments
|
||||||
|
mpv_no_config = false,
|
||||||
|
-- Add a "--profile=<mpv_profile>" to the mpv sub-call arguments
|
||||||
|
-- Use "" to disable
|
||||||
|
mpv_profile = "",
|
||||||
|
-- Output debug logs to <thumbnail_path>.log, ala <cache_directory>/<video_filename>/000000.bgra.log
|
||||||
|
-- The logs are removed after successful encodes, unless you set mpv_keep_logs below
|
||||||
|
mpv_logs = true,
|
||||||
|
-- Keep all mpv logs, even the succesfull ones
|
||||||
|
mpv_keep_logs = false,
|
||||||
|
|
||||||
|
-- Disable the built-in keybind ("T") to add your own
|
||||||
|
disable_keybinds = false,
|
||||||
|
|
||||||
|
---------------------
|
||||||
|
-- Display options --
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
-- Move the thumbnail up or down
|
||||||
|
-- For example:
|
||||||
|
-- topbar/bottombar: 24
|
||||||
|
-- rest: 0
|
||||||
|
vertical_offset = 24,
|
||||||
|
|
||||||
|
-- Adjust background padding
|
||||||
|
-- Examples:
|
||||||
|
-- topbar: 0, 10, 10, 10
|
||||||
|
-- bottombar: 10, 0, 10, 10
|
||||||
|
-- slimbox/box: 10, 10, 10, 10
|
||||||
|
pad_top = 10,
|
||||||
|
pad_bot = 0,
|
||||||
|
pad_left = 10,
|
||||||
|
pad_right = 10,
|
||||||
|
|
||||||
|
-- If true, pad values are screen-pixels. If false, video-pixels.
|
||||||
|
pad_in_screenspace = true,
|
||||||
|
-- Calculate pad into the offset
|
||||||
|
offset_by_pad = true,
|
||||||
|
|
||||||
|
-- Background color in BBGGRR
|
||||||
|
background_color = "000000",
|
||||||
|
-- Alpha: 0 - fully opaque, 255 - transparent
|
||||||
|
background_alpha = 80,
|
||||||
|
|
||||||
|
-- Keep thumbnail on the screen near left or right side
|
||||||
|
constrain_to_screen = true,
|
||||||
|
|
||||||
|
-- Do not display the thumbnailing progress
|
||||||
|
hide_progress = false,
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
-- Thumbnail options --
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
-- The maximum dimensions of the thumbnails (pixels)
|
||||||
|
thumbnail_width = 200,
|
||||||
|
thumbnail_height = 200,
|
||||||
|
|
||||||
|
-- The thumbnail count target
|
||||||
|
-- (This will result in a thumbnail every ~10 seconds for a 25 minute video)
|
||||||
|
thumbnail_count = 150,
|
||||||
|
|
||||||
|
-- The above target count will be adjusted by the minimum and
|
||||||
|
-- maximum time difference between thumbnails.
|
||||||
|
-- The thumbnail_count will be used to calculate a target separation,
|
||||||
|
-- and min/max_delta will be used to constrict it.
|
||||||
|
|
||||||
|
-- In other words, thumbnails will be:
|
||||||
|
-- at least min_delta seconds apart (limiting the amount)
|
||||||
|
-- at most max_delta seconds apart (raising the amount if needed)
|
||||||
|
min_delta = 5,
|
||||||
|
-- 120 seconds aka 2 minutes will add more thumbnails when the video is over 5 hours!
|
||||||
|
max_delta = 90,
|
||||||
|
|
||||||
|
|
||||||
|
-- Overrides for remote urls (you generally want less thumbnails!)
|
||||||
|
-- Thumbnailing network paths will be done with mpv
|
||||||
|
|
||||||
|
-- Allow thumbnailing network paths (naive check for "://")
|
||||||
|
thumbnail_network = false,
|
||||||
|
-- Override thumbnail count, min/max delta
|
||||||
|
remote_thumbnail_count = 60,
|
||||||
|
remote_min_delta = 15,
|
||||||
|
remote_max_delta = 120,
|
||||||
|
|
||||||
|
-- Try to grab the raw stream and disable ytdl for the mpv subcalls
|
||||||
|
-- Much faster than passing the url to ytdl again, but may cause problems with some sites
|
||||||
|
remote_direct_stream = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
read_options(thumbnailer_options, SCRIPT_NAME)
|
||||||
|
function skip_nil(tbl)
|
||||||
|
local n = {}
|
||||||
|
for k, v in pairs(tbl) do
|
||||||
|
table.insert(n, v)
|
||||||
|
end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
function create_thumbnail_mpv(file_path, timestamp, size, output_path, options)
|
||||||
|
options = options or {}
|
||||||
|
|
||||||
|
local ytdl_disabled = not options.enable_ytdl and (mp.get_property_native("ytdl") == false
|
||||||
|
or thumbnailer_options.remote_direct_stream)
|
||||||
|
|
||||||
|
local header_fields_arg = nil
|
||||||
|
local header_fields = mp.get_property_native("http-header-fields")
|
||||||
|
if #header_fields > 0 then
|
||||||
|
-- We can't escape the headers, mpv won't parse "--http-header-fields='Name: value'" properly
|
||||||
|
header_fields_arg = "--http-header-fields=" .. table.concat(header_fields, ",")
|
||||||
|
end
|
||||||
|
|
||||||
|
local profile_arg = nil
|
||||||
|
if thumbnailer_options.mpv_profile ~= "" then
|
||||||
|
profile_arg = "--profile=" .. thumbnailer_options.mpv_profile
|
||||||
|
end
|
||||||
|
|
||||||
|
local log_arg = "--log-file=" .. output_path .. ".log"
|
||||||
|
|
||||||
|
local mpv_command = skip_nil({
|
||||||
|
"mpv",
|
||||||
|
-- Hide console output
|
||||||
|
"--msg-level=all=no",
|
||||||
|
|
||||||
|
-- Disable ytdl
|
||||||
|
(ytdl_disabled and "--no-ytdl" or nil),
|
||||||
|
-- Pass HTTP headers from current instance
|
||||||
|
header_fields_arg,
|
||||||
|
-- Pass User-Agent and Referer - should do no harm even with ytdl active
|
||||||
|
"--user-agent=" .. mp.get_property_native("user-agent"),
|
||||||
|
"--referrer=" .. mp.get_property_native("referrer"),
|
||||||
|
-- Disable hardware decoding
|
||||||
|
"--hwdec=no",
|
||||||
|
|
||||||
|
-- Insert --no-config, --profile=... and --log-file if enabled
|
||||||
|
(thumbnailer_options.mpv_no_config and "--no-config" or nil),
|
||||||
|
profile_arg,
|
||||||
|
(thumbnailer_options.mpv_logs and log_arg or nil),
|
||||||
|
|
||||||
|
file_path,
|
||||||
|
|
||||||
|
"--start=" .. tostring(timestamp),
|
||||||
|
"--frames=1",
|
||||||
|
"--hr-seek=yes",
|
||||||
|
"--no-audio",
|
||||||
|
-- Optionally disable subtitles
|
||||||
|
(thumbnailer_options.mpv_no_sub and "--no-sub" or nil),
|
||||||
|
|
||||||
|
("--vf=scale=%d:%d"):format(size.w, size.h),
|
||||||
|
"--vf-add=format=bgra",
|
||||||
|
"--of=rawvideo",
|
||||||
|
"--ovc=rawvideo",
|
||||||
|
("--o=%s"):format(output_path)
|
||||||
|
})
|
||||||
|
return utils.subprocess({args=mpv_command})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function create_thumbnail_ffmpeg(file_path, timestamp, size, output_path)
|
||||||
|
local ffmpeg_command = {
|
||||||
|
"ffmpeg",
|
||||||
|
"-loglevel", "quiet",
|
||||||
|
"-noaccurate_seek",
|
||||||
|
"-ss", format_time(timestamp, ":"),
|
||||||
|
"-i", file_path,
|
||||||
|
|
||||||
|
"-frames:v", "1",
|
||||||
|
"-an",
|
||||||
|
|
||||||
|
"-vf", ("scale=%d:%d"):format(size.w, size.h),
|
||||||
|
"-c:v", "rawvideo",
|
||||||
|
"-pix_fmt", "bgra",
|
||||||
|
"-f", "rawvideo",
|
||||||
|
|
||||||
|
"-y", output_path
|
||||||
|
}
|
||||||
|
return utils.subprocess({args=ffmpeg_command})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function check_output(ret, output_path, is_mpv)
|
||||||
|
local log_path = output_path .. ".log"
|
||||||
|
local success = true
|
||||||
|
|
||||||
|
if ret.killed_by_us then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
if ret.error or ret.status ~= 0 then
|
||||||
|
msg.error("Thumbnailing command failed!")
|
||||||
|
msg.error("mpv process error:", ret.error)
|
||||||
|
msg.error("Process stdout:", ret.stdout)
|
||||||
|
if is_mpv then
|
||||||
|
msg.error("Debug log:", log_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
success = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if not file_exists(output_path) then
|
||||||
|
msg.error("Output file missing!", output_path)
|
||||||
|
success = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_mpv and not thumbnailer_options.mpv_keep_logs then
|
||||||
|
-- Remove successful debug logs
|
||||||
|
if success and file_exists(log_path) then
|
||||||
|
os.remove(log_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return success
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function do_worker_job(state_json_string, frames_json_string)
|
||||||
|
msg.debug("Handling given job")
|
||||||
|
local thumb_state, err = utils.parse_json(state_json_string)
|
||||||
|
if err then
|
||||||
|
msg.error("Failed to parse state JSON")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local thumbnail_indexes, err = utils.parse_json(frames_json_string)
|
||||||
|
if err then
|
||||||
|
msg.error("Failed to parse thumbnail frame indexes")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local thumbnail_func = create_thumbnail_mpv
|
||||||
|
if not thumbnailer_options.prefer_mpv then
|
||||||
|
if ExecutableFinder:get_executable_path("ffmpeg") then
|
||||||
|
thumbnail_func = create_thumbnail_ffmpeg
|
||||||
|
else
|
||||||
|
msg.warn("Could not find ffmpeg in PATH! Falling back on mpv.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local file_duration = mp.get_property_native("duration")
|
||||||
|
local file_path = thumb_state.worker_input_path
|
||||||
|
|
||||||
|
if thumb_state.is_remote then
|
||||||
|
if (thumbnail_func == create_thumbnail_ffmpeg) then
|
||||||
|
msg.warn("Thumbnailing remote path, falling back on mpv.")
|
||||||
|
end
|
||||||
|
thumbnail_func = create_thumbnail_mpv
|
||||||
|
end
|
||||||
|
|
||||||
|
local generate_thumbnail_for_index = function(thumbnail_index)
|
||||||
|
-- Given a 1-based thumbnail index, generate a thumbnail for it based on the thumbnailer state
|
||||||
|
local thumb_idx = thumbnail_index - 1
|
||||||
|
msg.debug("Starting work on thumbnail", thumb_idx)
|
||||||
|
|
||||||
|
local thumbnail_path = thumb_state.thumbnail_template:format(thumb_idx)
|
||||||
|
-- Grab the "middle" of the thumbnail duration instead of the very start, and leave some margin in the end
|
||||||
|
local timestamp = math.min(file_duration - 0.25, (thumb_idx + 0.5) * thumb_state.thumbnail_delta)
|
||||||
|
|
||||||
|
mp.commandv("script-message", "mpv_thumbnail_script-progress", tostring(thumbnail_index))
|
||||||
|
|
||||||
|
-- The expected size (raw BGRA image)
|
||||||
|
local thumbnail_raw_size = (thumb_state.thumbnail_size.w * thumb_state.thumbnail_size.h * 4)
|
||||||
|
|
||||||
|
local need_thumbnail_generation = false
|
||||||
|
|
||||||
|
-- Check if the thumbnail already exists and is the correct size
|
||||||
|
local thumbnail_file = io.open(thumbnail_path, "rb")
|
||||||
|
if thumbnail_file == nil then
|
||||||
|
need_thumbnail_generation = true
|
||||||
|
else
|
||||||
|
local existing_thumbnail_filesize = thumbnail_file:seek("end")
|
||||||
|
if existing_thumbnail_filesize ~= thumbnail_raw_size then
|
||||||
|
-- Size doesn't match, so (re)generate
|
||||||
|
msg.warn("Thumbnail", thumb_idx, "did not match expected size, regenerating")
|
||||||
|
need_thumbnail_generation = true
|
||||||
|
end
|
||||||
|
thumbnail_file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
if need_thumbnail_generation then
|
||||||
|
local ret = thumbnail_func(file_path, timestamp, thumb_state.thumbnail_size, thumbnail_path, thumb_state.worker_extra)
|
||||||
|
local success = check_output(ret, thumbnail_path, thumbnail_func == create_thumbnail_mpv)
|
||||||
|
|
||||||
|
if success == nil then
|
||||||
|
-- Killed by us, changing files, ignore
|
||||||
|
msg.debug("Changing files, subprocess killed")
|
||||||
|
return true
|
||||||
|
elseif not success then
|
||||||
|
-- Real failure
|
||||||
|
mp.osd_message("Thumbnailing failed, check console for details", 3.5)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
msg.debug("Thumbnail", thumb_idx, "already done!")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Verify thumbnail size
|
||||||
|
-- Sometimes ffmpeg will output an empty file when seeking to a "bad" section (usually the end)
|
||||||
|
thumbnail_file = io.open(thumbnail_path, "rb")
|
||||||
|
|
||||||
|
-- Bail if we can't read the file (it should really exist by now, we checked this in check_output!)
|
||||||
|
if thumbnail_file == nil then
|
||||||
|
msg.error("Thumbnail suddenly disappeared!")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check the size of the generated file
|
||||||
|
local thumbnail_file_size = thumbnail_file:seek("end")
|
||||||
|
thumbnail_file:close()
|
||||||
|
|
||||||
|
-- Check if the file is big enough
|
||||||
|
local missing_bytes = math.max(0, thumbnail_raw_size - thumbnail_file_size)
|
||||||
|
if missing_bytes > 0 then
|
||||||
|
msg.warn(("Thumbnail missing %d bytes (expected %d, had %d), padding %s"):format(
|
||||||
|
missing_bytes, thumbnail_raw_size, thumbnail_file_size, thumbnail_path
|
||||||
|
))
|
||||||
|
-- Pad the file if it's missing content (eg. ffmpeg seek to file end)
|
||||||
|
thumbnail_file = io.open(thumbnail_path, "ab")
|
||||||
|
thumbnail_file:write(string.rep(string.char(0), missing_bytes))
|
||||||
|
thumbnail_file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.debug("Finished work on thumbnail", thumb_idx)
|
||||||
|
mp.commandv("script-message", "mpv_thumbnail_script-ready", tostring(thumbnail_index), thumbnail_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.debug(("Generating %d thumbnails @ %dx%d for %q"):format(
|
||||||
|
#thumbnail_indexes,
|
||||||
|
thumb_state.thumbnail_size.w,
|
||||||
|
thumb_state.thumbnail_size.h,
|
||||||
|
file_path))
|
||||||
|
|
||||||
|
for i, thumbnail_index in ipairs(thumbnail_indexes) do
|
||||||
|
local bail = generate_thumbnail_for_index(thumbnail_index)
|
||||||
|
if bail then return end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set up listeners and keybinds
|
||||||
|
|
||||||
|
-- Job listener
|
||||||
|
mp.register_script_message("mpv_thumbnail_script-job", do_worker_job)
|
||||||
|
|
||||||
|
|
||||||
|
-- Register this worker with the master script
|
||||||
|
local register_timer = nil
|
||||||
|
local register_timeout = mp.get_time() + 1.5
|
||||||
|
|
||||||
|
local register_function = function()
|
||||||
|
if mp.get_time() > register_timeout and register_timer then
|
||||||
|
msg.error("Thumbnail worker registering timed out")
|
||||||
|
register_timer:stop()
|
||||||
|
else
|
||||||
|
msg.debug("Announcing self to master...")
|
||||||
|
mp.commandv("script-message", "mpv_thumbnail_script-worker", mp.get_script_name())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
register_timer = mp.add_periodic_timer(0.1, register_function)
|
||||||
|
|
||||||
|
mp.register_script_message("mpv_thumbnail_script-slaved", function()
|
||||||
|
msg.debug("Successfully registered with master")
|
||||||
|
register_timer:stop()
|
||||||
|
end)
|
||||||
736
config/mpv/scripts/mpv_thumbnail_script_server-2.lua
Normal file
736
config/mpv/scripts/mpv_thumbnail_script_server-2.lua
Normal file
|
|
@ -0,0 +1,736 @@
|
||||||
|
--[[
|
||||||
|
Copyright (C) 2017 AMM
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
]]--
|
||||||
|
--[[
|
||||||
|
mpv_thumbnail_script.lua 0.4.7 - commit 6282073 (branch master)
|
||||||
|
https://github.com/TheAMM/mpv_thumbnail_script
|
||||||
|
Built on 2022-02-05 16:00:24
|
||||||
|
]]--
|
||||||
|
local assdraw = require 'mp.assdraw'
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local opt = require 'mp.options'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
|
||||||
|
-- Determine platform --
|
||||||
|
ON_WINDOWS = (package.config:sub(1,1) ~= '/')
|
||||||
|
|
||||||
|
-- Some helper functions needed to parse the options --
|
||||||
|
function isempty(v) return (v == false) or (v == nil) or (v == "") or (v == 0) or (type(v) == "table" and next(v) == nil) end
|
||||||
|
|
||||||
|
function divmod (a, b)
|
||||||
|
return math.floor(a / b), a % b
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Better modulo
|
||||||
|
function bmod( i, N )
|
||||||
|
return (i % N + N) % N
|
||||||
|
end
|
||||||
|
|
||||||
|
function join_paths(...)
|
||||||
|
local sep = ON_WINDOWS and "\\" or "/"
|
||||||
|
local result = "";
|
||||||
|
for i, p in pairs({...}) do
|
||||||
|
if p ~= "" then
|
||||||
|
if is_absolute_path(p) then
|
||||||
|
result = p
|
||||||
|
else
|
||||||
|
result = (result ~= "") and (result:gsub("[\\"..sep.."]*$", "") .. sep .. p) or p
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result:gsub("[\\"..sep.."]*$", "")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- /some/path/file.ext -> /some/path, file.ext
|
||||||
|
function split_path( path )
|
||||||
|
local sep = ON_WINDOWS and "\\" or "/"
|
||||||
|
local first_index, last_index = path:find('^.*' .. sep)
|
||||||
|
|
||||||
|
if last_index == nil then
|
||||||
|
return "", path
|
||||||
|
else
|
||||||
|
local dir = path:sub(0, last_index-1)
|
||||||
|
local file = path:sub(last_index+1, -1)
|
||||||
|
|
||||||
|
return dir, file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function is_absolute_path( path )
|
||||||
|
local tmp, is_win = path:gsub("^[A-Z]:\\", "")
|
||||||
|
local tmp, is_unix = path:gsub("^/", "")
|
||||||
|
return (is_win > 0) or (is_unix > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Set(source)
|
||||||
|
local set = {}
|
||||||
|
for _, l in ipairs(source) do set[l] = true end
|
||||||
|
return set
|
||||||
|
end
|
||||||
|
|
||||||
|
---------------------------
|
||||||
|
-- More helper functions --
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
-- Removes all keys from a table, without destroying the reference to it
|
||||||
|
function clear_table(target)
|
||||||
|
for key, value in pairs(target) do
|
||||||
|
target[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function shallow_copy(target)
|
||||||
|
local copy = {}
|
||||||
|
for k, v in pairs(target) do
|
||||||
|
copy[k] = v
|
||||||
|
end
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rounds to given decimals. eg. round_dec(3.145, 0) => 3
|
||||||
|
function round_dec(num, idp)
|
||||||
|
local mult = 10^(idp or 0)
|
||||||
|
return math.floor(num * mult + 0.5) / mult
|
||||||
|
end
|
||||||
|
|
||||||
|
function file_exists(name)
|
||||||
|
local f = io.open(name, "rb")
|
||||||
|
if f ~= nil then
|
||||||
|
local ok, err, code = f:read(1)
|
||||||
|
io.close(f)
|
||||||
|
return code == nil
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function path_exists(name)
|
||||||
|
local f = io.open(name, "rb")
|
||||||
|
if f ~= nil then
|
||||||
|
io.close(f)
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function create_directories(path)
|
||||||
|
local cmd
|
||||||
|
if ON_WINDOWS then
|
||||||
|
cmd = { args = {"cmd", "/c", "mkdir", path} }
|
||||||
|
else
|
||||||
|
cmd = { args = {"mkdir", "-p", path} }
|
||||||
|
end
|
||||||
|
utils.subprocess(cmd)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find an executable in PATH or CWD with the given name
|
||||||
|
function find_executable(name)
|
||||||
|
local delim = ON_WINDOWS and ";" or ":"
|
||||||
|
|
||||||
|
local pwd = os.getenv("PWD") or utils.getcwd()
|
||||||
|
local path = os.getenv("PATH")
|
||||||
|
|
||||||
|
local env_path = pwd .. delim .. path -- Check CWD first
|
||||||
|
|
||||||
|
local result, filename
|
||||||
|
for path_dir in env_path:gmatch("[^"..delim.."]+") do
|
||||||
|
filename = join_paths(path_dir, name)
|
||||||
|
if file_exists(filename) then
|
||||||
|
result = filename
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local ExecutableFinder = { path_cache = {} }
|
||||||
|
-- Searches for an executable and caches the result if any
|
||||||
|
function ExecutableFinder:get_executable_path( name, raw_name )
|
||||||
|
name = ON_WINDOWS and not raw_name and (name .. ".exe") or name
|
||||||
|
|
||||||
|
if self.path_cache[name] == nil then
|
||||||
|
self.path_cache[name] = find_executable(name) or false
|
||||||
|
end
|
||||||
|
return self.path_cache[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Format seconds to HH.MM.SS.sss
|
||||||
|
function format_time(seconds, sep, decimals)
|
||||||
|
decimals = decimals == nil and 3 or decimals
|
||||||
|
sep = sep and sep or "."
|
||||||
|
local s = seconds
|
||||||
|
local h, s = divmod(s, 60*60)
|
||||||
|
local m, s = divmod(s, 60)
|
||||||
|
|
||||||
|
local second_format = string.format("%%0%d.%df", 2+(decimals > 0 and decimals+1 or 0), decimals)
|
||||||
|
|
||||||
|
return string.format("%02d"..sep.."%02d"..sep..second_format, h, m, s)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Format seconds to 1h 2m 3.4s
|
||||||
|
function format_time_hms(seconds, sep, decimals, force_full)
|
||||||
|
decimals = decimals == nil and 1 or decimals
|
||||||
|
sep = sep ~= nil and sep or " "
|
||||||
|
|
||||||
|
local s = seconds
|
||||||
|
local h, s = divmod(s, 60*60)
|
||||||
|
local m, s = divmod(s, 60)
|
||||||
|
|
||||||
|
if force_full or h > 0 then
|
||||||
|
return string.format("%dh"..sep.."%dm"..sep.."%." .. tostring(decimals) .. "fs", h, m, s)
|
||||||
|
elseif m > 0 then
|
||||||
|
return string.format("%dm"..sep.."%." .. tostring(decimals) .. "fs", m, s)
|
||||||
|
else
|
||||||
|
return string.format("%." .. tostring(decimals) .. "fs", s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Writes text on OSD and console
|
||||||
|
function log_info(txt, timeout)
|
||||||
|
timeout = timeout or 1.5
|
||||||
|
msg.info(txt)
|
||||||
|
mp.osd_message(txt, timeout)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Join table items, ala ({"a", "b", "c"}, "=", "-", ", ") => "=a-, =b-, =c-"
|
||||||
|
function join_table(source, before, after, sep)
|
||||||
|
before = before or ""
|
||||||
|
after = after or ""
|
||||||
|
sep = sep or ", "
|
||||||
|
local result = ""
|
||||||
|
for i, v in pairs(source) do
|
||||||
|
if not isempty(v) then
|
||||||
|
local part = before .. v .. after
|
||||||
|
if i == 1 then
|
||||||
|
result = part
|
||||||
|
else
|
||||||
|
result = result .. sep .. part
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function wrap(s, char)
|
||||||
|
char = char or "'"
|
||||||
|
return char .. s .. char
|
||||||
|
end
|
||||||
|
-- Wraps given string into 'string' and escapes any 's in it
|
||||||
|
function escape_and_wrap(s, char, replacement)
|
||||||
|
char = char or "'"
|
||||||
|
replacement = replacement or "\\" .. char
|
||||||
|
return wrap(string.gsub(s, char, replacement), char)
|
||||||
|
end
|
||||||
|
-- Escapes single quotes in a string and wraps the input in single quotes
|
||||||
|
function escape_single_bash(s)
|
||||||
|
return escape_and_wrap(s, "'", "'\\''")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns (a .. b) if b is not empty or nil
|
||||||
|
function joined_or_nil(a, b)
|
||||||
|
return not isempty(b) and (a .. b) or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Put items from one table into another
|
||||||
|
function extend_table(target, source)
|
||||||
|
for i, v in pairs(source) do
|
||||||
|
table.insert(target, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Creates a handle and filename for a temporary random file (in current directory)
|
||||||
|
function create_temporary_file(base, mode, suffix)
|
||||||
|
local handle, filename
|
||||||
|
suffix = suffix or ""
|
||||||
|
while true do
|
||||||
|
filename = base .. tostring(math.random(1, 5000)) .. suffix
|
||||||
|
handle = io.open(filename, "r")
|
||||||
|
if not handle then
|
||||||
|
handle = io.open(filename, mode)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
io.close(handle)
|
||||||
|
end
|
||||||
|
return handle, filename
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function get_processor_count()
|
||||||
|
local proc_count
|
||||||
|
|
||||||
|
if ON_WINDOWS then
|
||||||
|
proc_count = tonumber(os.getenv("NUMBER_OF_PROCESSORS"))
|
||||||
|
else
|
||||||
|
local cpuinfo_handle = io.open("/proc/cpuinfo")
|
||||||
|
if cpuinfo_handle ~= nil then
|
||||||
|
local cpuinfo_contents = cpuinfo_handle:read("*a")
|
||||||
|
local _, replace_count = cpuinfo_contents:gsub('processor', '')
|
||||||
|
proc_count = replace_count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if proc_count and proc_count > 0 then
|
||||||
|
return proc_count
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function substitute_values(string, values)
|
||||||
|
local substitutor = function(match)
|
||||||
|
if match == "%" then
|
||||||
|
return "%"
|
||||||
|
else
|
||||||
|
-- nil is discarded by gsub
|
||||||
|
return values[match]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local substituted = string:gsub('%%(.)', substitutor)
|
||||||
|
return substituted
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ASS HELPERS --
|
||||||
|
function round_rect_top( ass, x0, y0, x1, y1, r )
|
||||||
|
local c = 0.551915024494 * r -- circle approximation
|
||||||
|
ass:move_to(x0 + r, y0)
|
||||||
|
ass:line_to(x1 - r, y0) -- top line
|
||||||
|
if r > 0 then
|
||||||
|
ass:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r) -- top right corner
|
||||||
|
end
|
||||||
|
ass:line_to(x1, y1) -- right line
|
||||||
|
ass:line_to(x0, y1) -- bottom line
|
||||||
|
ass:line_to(x0, y0 + r) -- left line
|
||||||
|
if r > 0 then
|
||||||
|
ass:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0) -- top left corner
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function round_rect(ass, x0, y0, x1, y1, rtl, rtr, rbr, rbl)
|
||||||
|
local c = 0.551915024494
|
||||||
|
ass:move_to(x0 + rtl, y0)
|
||||||
|
ass:line_to(x1 - rtr, y0) -- top line
|
||||||
|
if rtr > 0 then
|
||||||
|
ass:bezier_curve(x1 - rtr + rtr*c, y0, x1, y0 + rtr - rtr*c, x1, y0 + rtr) -- top right corner
|
||||||
|
end
|
||||||
|
ass:line_to(x1, y1 - rbr) -- right line
|
||||||
|
if rbr > 0 then
|
||||||
|
ass:bezier_curve(x1, y1 - rbr + rbr*c, x1 - rbr + rbr*c, y1, x1 - rbr, y1) -- bottom right corner
|
||||||
|
end
|
||||||
|
ass:line_to(x0 + rbl, y1) -- bottom line
|
||||||
|
if rbl > 0 then
|
||||||
|
ass:bezier_curve(x0 + rbl - rbl*c, y1, x0, y1 - rbl + rbl*c, x0, y1 - rbl) -- bottom left corner
|
||||||
|
end
|
||||||
|
ass:line_to(x0, y0 + rtl) -- left line
|
||||||
|
if rtl > 0 then
|
||||||
|
ass:bezier_curve(x0, y0 + rtl - rtl*c, x0 + rtl - rtl*c, y0, x0 + rtl, y0) -- top left corner
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local SCRIPT_NAME = "mpv_thumbnail_script"
|
||||||
|
|
||||||
|
local default_cache_base = ON_WINDOWS and os.getenv("TEMP") or "/tmp/"
|
||||||
|
|
||||||
|
local thumbnailer_options = {
|
||||||
|
-- The thumbnail directory
|
||||||
|
cache_directory = join_paths(default_cache_base, "mpv_thumbs_cache"),
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
-- Generation options --
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
-- Automatically generate the thumbnails on video load, without a keypress
|
||||||
|
autogenerate = true,
|
||||||
|
|
||||||
|
-- Only automatically thumbnail videos shorter than this (seconds)
|
||||||
|
autogenerate_max_duration = 3600, -- 1 hour
|
||||||
|
|
||||||
|
-- SHA1-sum filenames over this length
|
||||||
|
-- It's nice to know what files the thumbnails are (hence directory names)
|
||||||
|
-- but long URLs may approach filesystem limits.
|
||||||
|
hash_filename_length = 128,
|
||||||
|
|
||||||
|
-- Use mpv to generate thumbnail even if ffmpeg is found in PATH
|
||||||
|
-- ffmpeg does not handle ordered chapters (MKVs which rely on other MKVs)!
|
||||||
|
-- mpv is a bit slower, but has better support overall (eg. subtitles in the previews)
|
||||||
|
prefer_mpv = true,
|
||||||
|
|
||||||
|
-- Explicitly disable subtitles on the mpv sub-calls
|
||||||
|
mpv_no_sub = false,
|
||||||
|
-- Add a "--no-config" to the mpv sub-call arguments
|
||||||
|
mpv_no_config = false,
|
||||||
|
-- Add a "--profile=<mpv_profile>" to the mpv sub-call arguments
|
||||||
|
-- Use "" to disable
|
||||||
|
mpv_profile = "",
|
||||||
|
-- Output debug logs to <thumbnail_path>.log, ala <cache_directory>/<video_filename>/000000.bgra.log
|
||||||
|
-- The logs are removed after successful encodes, unless you set mpv_keep_logs below
|
||||||
|
mpv_logs = true,
|
||||||
|
-- Keep all mpv logs, even the succesfull ones
|
||||||
|
mpv_keep_logs = false,
|
||||||
|
|
||||||
|
-- Disable the built-in keybind ("T") to add your own
|
||||||
|
disable_keybinds = false,
|
||||||
|
|
||||||
|
---------------------
|
||||||
|
-- Display options --
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
-- Move the thumbnail up or down
|
||||||
|
-- For example:
|
||||||
|
-- topbar/bottombar: 24
|
||||||
|
-- rest: 0
|
||||||
|
vertical_offset = 24,
|
||||||
|
|
||||||
|
-- Adjust background padding
|
||||||
|
-- Examples:
|
||||||
|
-- topbar: 0, 10, 10, 10
|
||||||
|
-- bottombar: 10, 0, 10, 10
|
||||||
|
-- slimbox/box: 10, 10, 10, 10
|
||||||
|
pad_top = 10,
|
||||||
|
pad_bot = 0,
|
||||||
|
pad_left = 10,
|
||||||
|
pad_right = 10,
|
||||||
|
|
||||||
|
-- If true, pad values are screen-pixels. If false, video-pixels.
|
||||||
|
pad_in_screenspace = true,
|
||||||
|
-- Calculate pad into the offset
|
||||||
|
offset_by_pad = true,
|
||||||
|
|
||||||
|
-- Background color in BBGGRR
|
||||||
|
background_color = "000000",
|
||||||
|
-- Alpha: 0 - fully opaque, 255 - transparent
|
||||||
|
background_alpha = 80,
|
||||||
|
|
||||||
|
-- Keep thumbnail on the screen near left or right side
|
||||||
|
constrain_to_screen = true,
|
||||||
|
|
||||||
|
-- Do not display the thumbnailing progress
|
||||||
|
hide_progress = false,
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
-- Thumbnail options --
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
-- The maximum dimensions of the thumbnails (pixels)
|
||||||
|
thumbnail_width = 200,
|
||||||
|
thumbnail_height = 200,
|
||||||
|
|
||||||
|
-- The thumbnail count target
|
||||||
|
-- (This will result in a thumbnail every ~10 seconds for a 25 minute video)
|
||||||
|
thumbnail_count = 150,
|
||||||
|
|
||||||
|
-- The above target count will be adjusted by the minimum and
|
||||||
|
-- maximum time difference between thumbnails.
|
||||||
|
-- The thumbnail_count will be used to calculate a target separation,
|
||||||
|
-- and min/max_delta will be used to constrict it.
|
||||||
|
|
||||||
|
-- In other words, thumbnails will be:
|
||||||
|
-- at least min_delta seconds apart (limiting the amount)
|
||||||
|
-- at most max_delta seconds apart (raising the amount if needed)
|
||||||
|
min_delta = 5,
|
||||||
|
-- 120 seconds aka 2 minutes will add more thumbnails when the video is over 5 hours!
|
||||||
|
max_delta = 90,
|
||||||
|
|
||||||
|
|
||||||
|
-- Overrides for remote urls (you generally want less thumbnails!)
|
||||||
|
-- Thumbnailing network paths will be done with mpv
|
||||||
|
|
||||||
|
-- Allow thumbnailing network paths (naive check for "://")
|
||||||
|
thumbnail_network = false,
|
||||||
|
-- Override thumbnail count, min/max delta
|
||||||
|
remote_thumbnail_count = 60,
|
||||||
|
remote_min_delta = 15,
|
||||||
|
remote_max_delta = 120,
|
||||||
|
|
||||||
|
-- Try to grab the raw stream and disable ytdl for the mpv subcalls
|
||||||
|
-- Much faster than passing the url to ytdl again, but may cause problems with some sites
|
||||||
|
remote_direct_stream = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
read_options(thumbnailer_options, SCRIPT_NAME)
|
||||||
|
function skip_nil(tbl)
|
||||||
|
local n = {}
|
||||||
|
for k, v in pairs(tbl) do
|
||||||
|
table.insert(n, v)
|
||||||
|
end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
function create_thumbnail_mpv(file_path, timestamp, size, output_path, options)
|
||||||
|
options = options or {}
|
||||||
|
|
||||||
|
local ytdl_disabled = not options.enable_ytdl and (mp.get_property_native("ytdl") == false
|
||||||
|
or thumbnailer_options.remote_direct_stream)
|
||||||
|
|
||||||
|
local header_fields_arg = nil
|
||||||
|
local header_fields = mp.get_property_native("http-header-fields")
|
||||||
|
if #header_fields > 0 then
|
||||||
|
-- We can't escape the headers, mpv won't parse "--http-header-fields='Name: value'" properly
|
||||||
|
header_fields_arg = "--http-header-fields=" .. table.concat(header_fields, ",")
|
||||||
|
end
|
||||||
|
|
||||||
|
local profile_arg = nil
|
||||||
|
if thumbnailer_options.mpv_profile ~= "" then
|
||||||
|
profile_arg = "--profile=" .. thumbnailer_options.mpv_profile
|
||||||
|
end
|
||||||
|
|
||||||
|
local log_arg = "--log-file=" .. output_path .. ".log"
|
||||||
|
|
||||||
|
local mpv_command = skip_nil({
|
||||||
|
"mpv",
|
||||||
|
-- Hide console output
|
||||||
|
"--msg-level=all=no",
|
||||||
|
|
||||||
|
-- Disable ytdl
|
||||||
|
(ytdl_disabled and "--no-ytdl" or nil),
|
||||||
|
-- Pass HTTP headers from current instance
|
||||||
|
header_fields_arg,
|
||||||
|
-- Pass User-Agent and Referer - should do no harm even with ytdl active
|
||||||
|
"--user-agent=" .. mp.get_property_native("user-agent"),
|
||||||
|
"--referrer=" .. mp.get_property_native("referrer"),
|
||||||
|
-- Disable hardware decoding
|
||||||
|
"--hwdec=no",
|
||||||
|
|
||||||
|
-- Insert --no-config, --profile=... and --log-file if enabled
|
||||||
|
(thumbnailer_options.mpv_no_config and "--no-config" or nil),
|
||||||
|
profile_arg,
|
||||||
|
(thumbnailer_options.mpv_logs and log_arg or nil),
|
||||||
|
|
||||||
|
file_path,
|
||||||
|
|
||||||
|
"--start=" .. tostring(timestamp),
|
||||||
|
"--frames=1",
|
||||||
|
"--hr-seek=yes",
|
||||||
|
"--no-audio",
|
||||||
|
-- Optionally disable subtitles
|
||||||
|
(thumbnailer_options.mpv_no_sub and "--no-sub" or nil),
|
||||||
|
|
||||||
|
("--vf=scale=%d:%d"):format(size.w, size.h),
|
||||||
|
"--vf-add=format=bgra",
|
||||||
|
"--of=rawvideo",
|
||||||
|
"--ovc=rawvideo",
|
||||||
|
("--o=%s"):format(output_path)
|
||||||
|
})
|
||||||
|
return utils.subprocess({args=mpv_command})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function create_thumbnail_ffmpeg(file_path, timestamp, size, output_path)
|
||||||
|
local ffmpeg_command = {
|
||||||
|
"ffmpeg",
|
||||||
|
"-loglevel", "quiet",
|
||||||
|
"-noaccurate_seek",
|
||||||
|
"-ss", format_time(timestamp, ":"),
|
||||||
|
"-i", file_path,
|
||||||
|
|
||||||
|
"-frames:v", "1",
|
||||||
|
"-an",
|
||||||
|
|
||||||
|
"-vf", ("scale=%d:%d"):format(size.w, size.h),
|
||||||
|
"-c:v", "rawvideo",
|
||||||
|
"-pix_fmt", "bgra",
|
||||||
|
"-f", "rawvideo",
|
||||||
|
|
||||||
|
"-y", output_path
|
||||||
|
}
|
||||||
|
return utils.subprocess({args=ffmpeg_command})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function check_output(ret, output_path, is_mpv)
|
||||||
|
local log_path = output_path .. ".log"
|
||||||
|
local success = true
|
||||||
|
|
||||||
|
if ret.killed_by_us then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
if ret.error or ret.status ~= 0 then
|
||||||
|
msg.error("Thumbnailing command failed!")
|
||||||
|
msg.error("mpv process error:", ret.error)
|
||||||
|
msg.error("Process stdout:", ret.stdout)
|
||||||
|
if is_mpv then
|
||||||
|
msg.error("Debug log:", log_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
success = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if not file_exists(output_path) then
|
||||||
|
msg.error("Output file missing!", output_path)
|
||||||
|
success = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_mpv and not thumbnailer_options.mpv_keep_logs then
|
||||||
|
-- Remove successful debug logs
|
||||||
|
if success and file_exists(log_path) then
|
||||||
|
os.remove(log_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return success
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function do_worker_job(state_json_string, frames_json_string)
|
||||||
|
msg.debug("Handling given job")
|
||||||
|
local thumb_state, err = utils.parse_json(state_json_string)
|
||||||
|
if err then
|
||||||
|
msg.error("Failed to parse state JSON")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local thumbnail_indexes, err = utils.parse_json(frames_json_string)
|
||||||
|
if err then
|
||||||
|
msg.error("Failed to parse thumbnail frame indexes")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local thumbnail_func = create_thumbnail_mpv
|
||||||
|
if not thumbnailer_options.prefer_mpv then
|
||||||
|
if ExecutableFinder:get_executable_path("ffmpeg") then
|
||||||
|
thumbnail_func = create_thumbnail_ffmpeg
|
||||||
|
else
|
||||||
|
msg.warn("Could not find ffmpeg in PATH! Falling back on mpv.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local file_duration = mp.get_property_native("duration")
|
||||||
|
local file_path = thumb_state.worker_input_path
|
||||||
|
|
||||||
|
if thumb_state.is_remote then
|
||||||
|
if (thumbnail_func == create_thumbnail_ffmpeg) then
|
||||||
|
msg.warn("Thumbnailing remote path, falling back on mpv.")
|
||||||
|
end
|
||||||
|
thumbnail_func = create_thumbnail_mpv
|
||||||
|
end
|
||||||
|
|
||||||
|
local generate_thumbnail_for_index = function(thumbnail_index)
|
||||||
|
-- Given a 1-based thumbnail index, generate a thumbnail for it based on the thumbnailer state
|
||||||
|
local thumb_idx = thumbnail_index - 1
|
||||||
|
msg.debug("Starting work on thumbnail", thumb_idx)
|
||||||
|
|
||||||
|
local thumbnail_path = thumb_state.thumbnail_template:format(thumb_idx)
|
||||||
|
-- Grab the "middle" of the thumbnail duration instead of the very start, and leave some margin in the end
|
||||||
|
local timestamp = math.min(file_duration - 0.25, (thumb_idx + 0.5) * thumb_state.thumbnail_delta)
|
||||||
|
|
||||||
|
mp.commandv("script-message", "mpv_thumbnail_script-progress", tostring(thumbnail_index))
|
||||||
|
|
||||||
|
-- The expected size (raw BGRA image)
|
||||||
|
local thumbnail_raw_size = (thumb_state.thumbnail_size.w * thumb_state.thumbnail_size.h * 4)
|
||||||
|
|
||||||
|
local need_thumbnail_generation = false
|
||||||
|
|
||||||
|
-- Check if the thumbnail already exists and is the correct size
|
||||||
|
local thumbnail_file = io.open(thumbnail_path, "rb")
|
||||||
|
if thumbnail_file == nil then
|
||||||
|
need_thumbnail_generation = true
|
||||||
|
else
|
||||||
|
local existing_thumbnail_filesize = thumbnail_file:seek("end")
|
||||||
|
if existing_thumbnail_filesize ~= thumbnail_raw_size then
|
||||||
|
-- Size doesn't match, so (re)generate
|
||||||
|
msg.warn("Thumbnail", thumb_idx, "did not match expected size, regenerating")
|
||||||
|
need_thumbnail_generation = true
|
||||||
|
end
|
||||||
|
thumbnail_file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
if need_thumbnail_generation then
|
||||||
|
local ret = thumbnail_func(file_path, timestamp, thumb_state.thumbnail_size, thumbnail_path, thumb_state.worker_extra)
|
||||||
|
local success = check_output(ret, thumbnail_path, thumbnail_func == create_thumbnail_mpv)
|
||||||
|
|
||||||
|
if success == nil then
|
||||||
|
-- Killed by us, changing files, ignore
|
||||||
|
msg.debug("Changing files, subprocess killed")
|
||||||
|
return true
|
||||||
|
elseif not success then
|
||||||
|
-- Real failure
|
||||||
|
mp.osd_message("Thumbnailing failed, check console for details", 3.5)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
msg.debug("Thumbnail", thumb_idx, "already done!")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Verify thumbnail size
|
||||||
|
-- Sometimes ffmpeg will output an empty file when seeking to a "bad" section (usually the end)
|
||||||
|
thumbnail_file = io.open(thumbnail_path, "rb")
|
||||||
|
|
||||||
|
-- Bail if we can't read the file (it should really exist by now, we checked this in check_output!)
|
||||||
|
if thumbnail_file == nil then
|
||||||
|
msg.error("Thumbnail suddenly disappeared!")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check the size of the generated file
|
||||||
|
local thumbnail_file_size = thumbnail_file:seek("end")
|
||||||
|
thumbnail_file:close()
|
||||||
|
|
||||||
|
-- Check if the file is big enough
|
||||||
|
local missing_bytes = math.max(0, thumbnail_raw_size - thumbnail_file_size)
|
||||||
|
if missing_bytes > 0 then
|
||||||
|
msg.warn(("Thumbnail missing %d bytes (expected %d, had %d), padding %s"):format(
|
||||||
|
missing_bytes, thumbnail_raw_size, thumbnail_file_size, thumbnail_path
|
||||||
|
))
|
||||||
|
-- Pad the file if it's missing content (eg. ffmpeg seek to file end)
|
||||||
|
thumbnail_file = io.open(thumbnail_path, "ab")
|
||||||
|
thumbnail_file:write(string.rep(string.char(0), missing_bytes))
|
||||||
|
thumbnail_file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.debug("Finished work on thumbnail", thumb_idx)
|
||||||
|
mp.commandv("script-message", "mpv_thumbnail_script-ready", tostring(thumbnail_index), thumbnail_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.debug(("Generating %d thumbnails @ %dx%d for %q"):format(
|
||||||
|
#thumbnail_indexes,
|
||||||
|
thumb_state.thumbnail_size.w,
|
||||||
|
thumb_state.thumbnail_size.h,
|
||||||
|
file_path))
|
||||||
|
|
||||||
|
for i, thumbnail_index in ipairs(thumbnail_indexes) do
|
||||||
|
local bail = generate_thumbnail_for_index(thumbnail_index)
|
||||||
|
if bail then return end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set up listeners and keybinds
|
||||||
|
|
||||||
|
-- Job listener
|
||||||
|
mp.register_script_message("mpv_thumbnail_script-job", do_worker_job)
|
||||||
|
|
||||||
|
|
||||||
|
-- Register this worker with the master script
|
||||||
|
local register_timer = nil
|
||||||
|
local register_timeout = mp.get_time() + 1.5
|
||||||
|
|
||||||
|
local register_function = function()
|
||||||
|
if mp.get_time() > register_timeout and register_timer then
|
||||||
|
msg.error("Thumbnail worker registering timed out")
|
||||||
|
register_timer:stop()
|
||||||
|
else
|
||||||
|
msg.debug("Announcing self to master...")
|
||||||
|
mp.commandv("script-message", "mpv_thumbnail_script-worker", mp.get_script_name())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
register_timer = mp.add_periodic_timer(0.1, register_function)
|
||||||
|
|
||||||
|
mp.register_script_message("mpv_thumbnail_script-slaved", function()
|
||||||
|
msg.debug("Successfully registered with master")
|
||||||
|
register_timer:stop()
|
||||||
|
end)
|
||||||
736
config/mpv/scripts/mpv_thumbnail_script_server.lua
Normal file
736
config/mpv/scripts/mpv_thumbnail_script_server.lua
Normal file
|
|
@ -0,0 +1,736 @@
|
||||||
|
--[[
|
||||||
|
Copyright (C) 2017 AMM
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
]]--
|
||||||
|
--[[
|
||||||
|
mpv_thumbnail_script.lua 0.4.7 - commit 6282073 (branch master)
|
||||||
|
https://github.com/TheAMM/mpv_thumbnail_script
|
||||||
|
Built on 2022-02-05 16:00:24
|
||||||
|
]]--
|
||||||
|
local assdraw = require 'mp.assdraw'
|
||||||
|
local msg = require 'mp.msg'
|
||||||
|
local opt = require 'mp.options'
|
||||||
|
local utils = require 'mp.utils'
|
||||||
|
|
||||||
|
-- Determine platform --
|
||||||
|
ON_WINDOWS = (package.config:sub(1,1) ~= '/')
|
||||||
|
|
||||||
|
-- Some helper functions needed to parse the options --
|
||||||
|
function isempty(v) return (v == false) or (v == nil) or (v == "") or (v == 0) or (type(v) == "table" and next(v) == nil) end
|
||||||
|
|
||||||
|
function divmod (a, b)
|
||||||
|
return math.floor(a / b), a % b
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Better modulo
|
||||||
|
function bmod( i, N )
|
||||||
|
return (i % N + N) % N
|
||||||
|
end
|
||||||
|
|
||||||
|
function join_paths(...)
|
||||||
|
local sep = ON_WINDOWS and "\\" or "/"
|
||||||
|
local result = "";
|
||||||
|
for i, p in pairs({...}) do
|
||||||
|
if p ~= "" then
|
||||||
|
if is_absolute_path(p) then
|
||||||
|
result = p
|
||||||
|
else
|
||||||
|
result = (result ~= "") and (result:gsub("[\\"..sep.."]*$", "") .. sep .. p) or p
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result:gsub("[\\"..sep.."]*$", "")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- /some/path/file.ext -> /some/path, file.ext
|
||||||
|
function split_path( path )
|
||||||
|
local sep = ON_WINDOWS and "\\" or "/"
|
||||||
|
local first_index, last_index = path:find('^.*' .. sep)
|
||||||
|
|
||||||
|
if last_index == nil then
|
||||||
|
return "", path
|
||||||
|
else
|
||||||
|
local dir = path:sub(0, last_index-1)
|
||||||
|
local file = path:sub(last_index+1, -1)
|
||||||
|
|
||||||
|
return dir, file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function is_absolute_path( path )
|
||||||
|
local tmp, is_win = path:gsub("^[A-Z]:\\", "")
|
||||||
|
local tmp, is_unix = path:gsub("^/", "")
|
||||||
|
return (is_win > 0) or (is_unix > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Set(source)
|
||||||
|
local set = {}
|
||||||
|
for _, l in ipairs(source) do set[l] = true end
|
||||||
|
return set
|
||||||
|
end
|
||||||
|
|
||||||
|
---------------------------
|
||||||
|
-- More helper functions --
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
-- Removes all keys from a table, without destroying the reference to it
|
||||||
|
function clear_table(target)
|
||||||
|
for key, value in pairs(target) do
|
||||||
|
target[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function shallow_copy(target)
|
||||||
|
local copy = {}
|
||||||
|
for k, v in pairs(target) do
|
||||||
|
copy[k] = v
|
||||||
|
end
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Rounds to given decimals. eg. round_dec(3.145, 0) => 3
|
||||||
|
function round_dec(num, idp)
|
||||||
|
local mult = 10^(idp or 0)
|
||||||
|
return math.floor(num * mult + 0.5) / mult
|
||||||
|
end
|
||||||
|
|
||||||
|
function file_exists(name)
|
||||||
|
local f = io.open(name, "rb")
|
||||||
|
if f ~= nil then
|
||||||
|
local ok, err, code = f:read(1)
|
||||||
|
io.close(f)
|
||||||
|
return code == nil
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function path_exists(name)
|
||||||
|
local f = io.open(name, "rb")
|
||||||
|
if f ~= nil then
|
||||||
|
io.close(f)
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function create_directories(path)
|
||||||
|
local cmd
|
||||||
|
if ON_WINDOWS then
|
||||||
|
cmd = { args = {"cmd", "/c", "mkdir", path} }
|
||||||
|
else
|
||||||
|
cmd = { args = {"mkdir", "-p", path} }
|
||||||
|
end
|
||||||
|
utils.subprocess(cmd)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find an executable in PATH or CWD with the given name
|
||||||
|
function find_executable(name)
|
||||||
|
local delim = ON_WINDOWS and ";" or ":"
|
||||||
|
|
||||||
|
local pwd = os.getenv("PWD") or utils.getcwd()
|
||||||
|
local path = os.getenv("PATH")
|
||||||
|
|
||||||
|
local env_path = pwd .. delim .. path -- Check CWD first
|
||||||
|
|
||||||
|
local result, filename
|
||||||
|
for path_dir in env_path:gmatch("[^"..delim.."]+") do
|
||||||
|
filename = join_paths(path_dir, name)
|
||||||
|
if file_exists(filename) then
|
||||||
|
result = filename
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
local ExecutableFinder = { path_cache = {} }
|
||||||
|
-- Searches for an executable and caches the result if any
|
||||||
|
function ExecutableFinder:get_executable_path( name, raw_name )
|
||||||
|
name = ON_WINDOWS and not raw_name and (name .. ".exe") or name
|
||||||
|
|
||||||
|
if self.path_cache[name] == nil then
|
||||||
|
self.path_cache[name] = find_executable(name) or false
|
||||||
|
end
|
||||||
|
return self.path_cache[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Format seconds to HH.MM.SS.sss
|
||||||
|
function format_time(seconds, sep, decimals)
|
||||||
|
decimals = decimals == nil and 3 or decimals
|
||||||
|
sep = sep and sep or "."
|
||||||
|
local s = seconds
|
||||||
|
local h, s = divmod(s, 60*60)
|
||||||
|
local m, s = divmod(s, 60)
|
||||||
|
|
||||||
|
local second_format = string.format("%%0%d.%df", 2+(decimals > 0 and decimals+1 or 0), decimals)
|
||||||
|
|
||||||
|
return string.format("%02d"..sep.."%02d"..sep..second_format, h, m, s)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Format seconds to 1h 2m 3.4s
|
||||||
|
function format_time_hms(seconds, sep, decimals, force_full)
|
||||||
|
decimals = decimals == nil and 1 or decimals
|
||||||
|
sep = sep ~= nil and sep or " "
|
||||||
|
|
||||||
|
local s = seconds
|
||||||
|
local h, s = divmod(s, 60*60)
|
||||||
|
local m, s = divmod(s, 60)
|
||||||
|
|
||||||
|
if force_full or h > 0 then
|
||||||
|
return string.format("%dh"..sep.."%dm"..sep.."%." .. tostring(decimals) .. "fs", h, m, s)
|
||||||
|
elseif m > 0 then
|
||||||
|
return string.format("%dm"..sep.."%." .. tostring(decimals) .. "fs", m, s)
|
||||||
|
else
|
||||||
|
return string.format("%." .. tostring(decimals) .. "fs", s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Writes text on OSD and console
|
||||||
|
function log_info(txt, timeout)
|
||||||
|
timeout = timeout or 1.5
|
||||||
|
msg.info(txt)
|
||||||
|
mp.osd_message(txt, timeout)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Join table items, ala ({"a", "b", "c"}, "=", "-", ", ") => "=a-, =b-, =c-"
|
||||||
|
function join_table(source, before, after, sep)
|
||||||
|
before = before or ""
|
||||||
|
after = after or ""
|
||||||
|
sep = sep or ", "
|
||||||
|
local result = ""
|
||||||
|
for i, v in pairs(source) do
|
||||||
|
if not isempty(v) then
|
||||||
|
local part = before .. v .. after
|
||||||
|
if i == 1 then
|
||||||
|
result = part
|
||||||
|
else
|
||||||
|
result = result .. sep .. part
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
function wrap(s, char)
|
||||||
|
char = char or "'"
|
||||||
|
return char .. s .. char
|
||||||
|
end
|
||||||
|
-- Wraps given string into 'string' and escapes any 's in it
|
||||||
|
function escape_and_wrap(s, char, replacement)
|
||||||
|
char = char or "'"
|
||||||
|
replacement = replacement or "\\" .. char
|
||||||
|
return wrap(string.gsub(s, char, replacement), char)
|
||||||
|
end
|
||||||
|
-- Escapes single quotes in a string and wraps the input in single quotes
|
||||||
|
function escape_single_bash(s)
|
||||||
|
return escape_and_wrap(s, "'", "'\\''")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns (a .. b) if b is not empty or nil
|
||||||
|
function joined_or_nil(a, b)
|
||||||
|
return not isempty(b) and (a .. b) or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Put items from one table into another
|
||||||
|
function extend_table(target, source)
|
||||||
|
for i, v in pairs(source) do
|
||||||
|
table.insert(target, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Creates a handle and filename for a temporary random file (in current directory)
|
||||||
|
function create_temporary_file(base, mode, suffix)
|
||||||
|
local handle, filename
|
||||||
|
suffix = suffix or ""
|
||||||
|
while true do
|
||||||
|
filename = base .. tostring(math.random(1, 5000)) .. suffix
|
||||||
|
handle = io.open(filename, "r")
|
||||||
|
if not handle then
|
||||||
|
handle = io.open(filename, mode)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
io.close(handle)
|
||||||
|
end
|
||||||
|
return handle, filename
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function get_processor_count()
|
||||||
|
local proc_count
|
||||||
|
|
||||||
|
if ON_WINDOWS then
|
||||||
|
proc_count = tonumber(os.getenv("NUMBER_OF_PROCESSORS"))
|
||||||
|
else
|
||||||
|
local cpuinfo_handle = io.open("/proc/cpuinfo")
|
||||||
|
if cpuinfo_handle ~= nil then
|
||||||
|
local cpuinfo_contents = cpuinfo_handle:read("*a")
|
||||||
|
local _, replace_count = cpuinfo_contents:gsub('processor', '')
|
||||||
|
proc_count = replace_count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if proc_count and proc_count > 0 then
|
||||||
|
return proc_count
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function substitute_values(string, values)
|
||||||
|
local substitutor = function(match)
|
||||||
|
if match == "%" then
|
||||||
|
return "%"
|
||||||
|
else
|
||||||
|
-- nil is discarded by gsub
|
||||||
|
return values[match]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local substituted = string:gsub('%%(.)', substitutor)
|
||||||
|
return substituted
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ASS HELPERS --
|
||||||
|
function round_rect_top( ass, x0, y0, x1, y1, r )
|
||||||
|
local c = 0.551915024494 * r -- circle approximation
|
||||||
|
ass:move_to(x0 + r, y0)
|
||||||
|
ass:line_to(x1 - r, y0) -- top line
|
||||||
|
if r > 0 then
|
||||||
|
ass:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r) -- top right corner
|
||||||
|
end
|
||||||
|
ass:line_to(x1, y1) -- right line
|
||||||
|
ass:line_to(x0, y1) -- bottom line
|
||||||
|
ass:line_to(x0, y0 + r) -- left line
|
||||||
|
if r > 0 then
|
||||||
|
ass:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0) -- top left corner
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function round_rect(ass, x0, y0, x1, y1, rtl, rtr, rbr, rbl)
|
||||||
|
local c = 0.551915024494
|
||||||
|
ass:move_to(x0 + rtl, y0)
|
||||||
|
ass:line_to(x1 - rtr, y0) -- top line
|
||||||
|
if rtr > 0 then
|
||||||
|
ass:bezier_curve(x1 - rtr + rtr*c, y0, x1, y0 + rtr - rtr*c, x1, y0 + rtr) -- top right corner
|
||||||
|
end
|
||||||
|
ass:line_to(x1, y1 - rbr) -- right line
|
||||||
|
if rbr > 0 then
|
||||||
|
ass:bezier_curve(x1, y1 - rbr + rbr*c, x1 - rbr + rbr*c, y1, x1 - rbr, y1) -- bottom right corner
|
||||||
|
end
|
||||||
|
ass:line_to(x0 + rbl, y1) -- bottom line
|
||||||
|
if rbl > 0 then
|
||||||
|
ass:bezier_curve(x0 + rbl - rbl*c, y1, x0, y1 - rbl + rbl*c, x0, y1 - rbl) -- bottom left corner
|
||||||
|
end
|
||||||
|
ass:line_to(x0, y0 + rtl) -- left line
|
||||||
|
if rtl > 0 then
|
||||||
|
ass:bezier_curve(x0, y0 + rtl - rtl*c, x0 + rtl - rtl*c, y0, x0 + rtl, y0) -- top left corner
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local SCRIPT_NAME = "mpv_thumbnail_script"
|
||||||
|
|
||||||
|
local default_cache_base = ON_WINDOWS and os.getenv("TEMP") or "/tmp/"
|
||||||
|
|
||||||
|
local thumbnailer_options = {
|
||||||
|
-- The thumbnail directory
|
||||||
|
cache_directory = join_paths(default_cache_base, "mpv_thumbs_cache"),
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
-- Generation options --
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
-- Automatically generate the thumbnails on video load, without a keypress
|
||||||
|
autogenerate = true,
|
||||||
|
|
||||||
|
-- Only automatically thumbnail videos shorter than this (seconds)
|
||||||
|
autogenerate_max_duration = 3600, -- 1 hour
|
||||||
|
|
||||||
|
-- SHA1-sum filenames over this length
|
||||||
|
-- It's nice to know what files the thumbnails are (hence directory names)
|
||||||
|
-- but long URLs may approach filesystem limits.
|
||||||
|
hash_filename_length = 128,
|
||||||
|
|
||||||
|
-- Use mpv to generate thumbnail even if ffmpeg is found in PATH
|
||||||
|
-- ffmpeg does not handle ordered chapters (MKVs which rely on other MKVs)!
|
||||||
|
-- mpv is a bit slower, but has better support overall (eg. subtitles in the previews)
|
||||||
|
prefer_mpv = true,
|
||||||
|
|
||||||
|
-- Explicitly disable subtitles on the mpv sub-calls
|
||||||
|
mpv_no_sub = false,
|
||||||
|
-- Add a "--no-config" to the mpv sub-call arguments
|
||||||
|
mpv_no_config = false,
|
||||||
|
-- Add a "--profile=<mpv_profile>" to the mpv sub-call arguments
|
||||||
|
-- Use "" to disable
|
||||||
|
mpv_profile = "",
|
||||||
|
-- Output debug logs to <thumbnail_path>.log, ala <cache_directory>/<video_filename>/000000.bgra.log
|
||||||
|
-- The logs are removed after successful encodes, unless you set mpv_keep_logs below
|
||||||
|
mpv_logs = true,
|
||||||
|
-- Keep all mpv logs, even the succesfull ones
|
||||||
|
mpv_keep_logs = false,
|
||||||
|
|
||||||
|
-- Disable the built-in keybind ("T") to add your own
|
||||||
|
disable_keybinds = false,
|
||||||
|
|
||||||
|
---------------------
|
||||||
|
-- Display options --
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
-- Move the thumbnail up or down
|
||||||
|
-- For example:
|
||||||
|
-- topbar/bottombar: 24
|
||||||
|
-- rest: 0
|
||||||
|
vertical_offset = 24,
|
||||||
|
|
||||||
|
-- Adjust background padding
|
||||||
|
-- Examples:
|
||||||
|
-- topbar: 0, 10, 10, 10
|
||||||
|
-- bottombar: 10, 0, 10, 10
|
||||||
|
-- slimbox/box: 10, 10, 10, 10
|
||||||
|
pad_top = 10,
|
||||||
|
pad_bot = 0,
|
||||||
|
pad_left = 10,
|
||||||
|
pad_right = 10,
|
||||||
|
|
||||||
|
-- If true, pad values are screen-pixels. If false, video-pixels.
|
||||||
|
pad_in_screenspace = true,
|
||||||
|
-- Calculate pad into the offset
|
||||||
|
offset_by_pad = true,
|
||||||
|
|
||||||
|
-- Background color in BBGGRR
|
||||||
|
background_color = "000000",
|
||||||
|
-- Alpha: 0 - fully opaque, 255 - transparent
|
||||||
|
background_alpha = 80,
|
||||||
|
|
||||||
|
-- Keep thumbnail on the screen near left or right side
|
||||||
|
constrain_to_screen = true,
|
||||||
|
|
||||||
|
-- Do not display the thumbnailing progress
|
||||||
|
hide_progress = false,
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
-- Thumbnail options --
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
-- The maximum dimensions of the thumbnails (pixels)
|
||||||
|
thumbnail_width = 200,
|
||||||
|
thumbnail_height = 200,
|
||||||
|
|
||||||
|
-- The thumbnail count target
|
||||||
|
-- (This will result in a thumbnail every ~10 seconds for a 25 minute video)
|
||||||
|
thumbnail_count = 150,
|
||||||
|
|
||||||
|
-- The above target count will be adjusted by the minimum and
|
||||||
|
-- maximum time difference between thumbnails.
|
||||||
|
-- The thumbnail_count will be used to calculate a target separation,
|
||||||
|
-- and min/max_delta will be used to constrict it.
|
||||||
|
|
||||||
|
-- In other words, thumbnails will be:
|
||||||
|
-- at least min_delta seconds apart (limiting the amount)
|
||||||
|
-- at most max_delta seconds apart (raising the amount if needed)
|
||||||
|
min_delta = 5,
|
||||||
|
-- 120 seconds aka 2 minutes will add more thumbnails when the video is over 5 hours!
|
||||||
|
max_delta = 90,
|
||||||
|
|
||||||
|
|
||||||
|
-- Overrides for remote urls (you generally want less thumbnails!)
|
||||||
|
-- Thumbnailing network paths will be done with mpv
|
||||||
|
|
||||||
|
-- Allow thumbnailing network paths (naive check for "://")
|
||||||
|
thumbnail_network = false,
|
||||||
|
-- Override thumbnail count, min/max delta
|
||||||
|
remote_thumbnail_count = 60,
|
||||||
|
remote_min_delta = 15,
|
||||||
|
remote_max_delta = 120,
|
||||||
|
|
||||||
|
-- Try to grab the raw stream and disable ytdl for the mpv subcalls
|
||||||
|
-- Much faster than passing the url to ytdl again, but may cause problems with some sites
|
||||||
|
remote_direct_stream = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
read_options(thumbnailer_options, SCRIPT_NAME)
|
||||||
|
function skip_nil(tbl)
|
||||||
|
local n = {}
|
||||||
|
for k, v in pairs(tbl) do
|
||||||
|
table.insert(n, v)
|
||||||
|
end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
function create_thumbnail_mpv(file_path, timestamp, size, output_path, options)
|
||||||
|
options = options or {}
|
||||||
|
|
||||||
|
local ytdl_disabled = not options.enable_ytdl and (mp.get_property_native("ytdl") == false
|
||||||
|
or thumbnailer_options.remote_direct_stream)
|
||||||
|
|
||||||
|
local header_fields_arg = nil
|
||||||
|
local header_fields = mp.get_property_native("http-header-fields")
|
||||||
|
if #header_fields > 0 then
|
||||||
|
-- We can't escape the headers, mpv won't parse "--http-header-fields='Name: value'" properly
|
||||||
|
header_fields_arg = "--http-header-fields=" .. table.concat(header_fields, ",")
|
||||||
|
end
|
||||||
|
|
||||||
|
local profile_arg = nil
|
||||||
|
if thumbnailer_options.mpv_profile ~= "" then
|
||||||
|
profile_arg = "--profile=" .. thumbnailer_options.mpv_profile
|
||||||
|
end
|
||||||
|
|
||||||
|
local log_arg = "--log-file=" .. output_path .. ".log"
|
||||||
|
|
||||||
|
local mpv_command = skip_nil({
|
||||||
|
"mpv",
|
||||||
|
-- Hide console output
|
||||||
|
"--msg-level=all=no",
|
||||||
|
|
||||||
|
-- Disable ytdl
|
||||||
|
(ytdl_disabled and "--no-ytdl" or nil),
|
||||||
|
-- Pass HTTP headers from current instance
|
||||||
|
header_fields_arg,
|
||||||
|
-- Pass User-Agent and Referer - should do no harm even with ytdl active
|
||||||
|
"--user-agent=" .. mp.get_property_native("user-agent"),
|
||||||
|
"--referrer=" .. mp.get_property_native("referrer"),
|
||||||
|
-- Disable hardware decoding
|
||||||
|
"--hwdec=no",
|
||||||
|
|
||||||
|
-- Insert --no-config, --profile=... and --log-file if enabled
|
||||||
|
(thumbnailer_options.mpv_no_config and "--no-config" or nil),
|
||||||
|
profile_arg,
|
||||||
|
(thumbnailer_options.mpv_logs and log_arg or nil),
|
||||||
|
|
||||||
|
file_path,
|
||||||
|
|
||||||
|
"--start=" .. tostring(timestamp),
|
||||||
|
"--frames=1",
|
||||||
|
"--hr-seek=yes",
|
||||||
|
"--no-audio",
|
||||||
|
-- Optionally disable subtitles
|
||||||
|
(thumbnailer_options.mpv_no_sub and "--no-sub" or nil),
|
||||||
|
|
||||||
|
("--vf=scale=%d:%d"):format(size.w, size.h),
|
||||||
|
"--vf-add=format=bgra",
|
||||||
|
"--of=rawvideo",
|
||||||
|
"--ovc=rawvideo",
|
||||||
|
("--o=%s"):format(output_path)
|
||||||
|
})
|
||||||
|
return utils.subprocess({args=mpv_command})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function create_thumbnail_ffmpeg(file_path, timestamp, size, output_path)
|
||||||
|
local ffmpeg_command = {
|
||||||
|
"ffmpeg",
|
||||||
|
"-loglevel", "quiet",
|
||||||
|
"-noaccurate_seek",
|
||||||
|
"-ss", format_time(timestamp, ":"),
|
||||||
|
"-i", file_path,
|
||||||
|
|
||||||
|
"-frames:v", "1",
|
||||||
|
"-an",
|
||||||
|
|
||||||
|
"-vf", ("scale=%d:%d"):format(size.w, size.h),
|
||||||
|
"-c:v", "rawvideo",
|
||||||
|
"-pix_fmt", "bgra",
|
||||||
|
"-f", "rawvideo",
|
||||||
|
|
||||||
|
"-y", output_path
|
||||||
|
}
|
||||||
|
return utils.subprocess({args=ffmpeg_command})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function check_output(ret, output_path, is_mpv)
|
||||||
|
local log_path = output_path .. ".log"
|
||||||
|
local success = true
|
||||||
|
|
||||||
|
if ret.killed_by_us then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
if ret.error or ret.status ~= 0 then
|
||||||
|
msg.error("Thumbnailing command failed!")
|
||||||
|
msg.error("mpv process error:", ret.error)
|
||||||
|
msg.error("Process stdout:", ret.stdout)
|
||||||
|
if is_mpv then
|
||||||
|
msg.error("Debug log:", log_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
success = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if not file_exists(output_path) then
|
||||||
|
msg.error("Output file missing!", output_path)
|
||||||
|
success = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_mpv and not thumbnailer_options.mpv_keep_logs then
|
||||||
|
-- Remove successful debug logs
|
||||||
|
if success and file_exists(log_path) then
|
||||||
|
os.remove(log_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return success
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function do_worker_job(state_json_string, frames_json_string)
|
||||||
|
msg.debug("Handling given job")
|
||||||
|
local thumb_state, err = utils.parse_json(state_json_string)
|
||||||
|
if err then
|
||||||
|
msg.error("Failed to parse state JSON")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local thumbnail_indexes, err = utils.parse_json(frames_json_string)
|
||||||
|
if err then
|
||||||
|
msg.error("Failed to parse thumbnail frame indexes")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local thumbnail_func = create_thumbnail_mpv
|
||||||
|
if not thumbnailer_options.prefer_mpv then
|
||||||
|
if ExecutableFinder:get_executable_path("ffmpeg") then
|
||||||
|
thumbnail_func = create_thumbnail_ffmpeg
|
||||||
|
else
|
||||||
|
msg.warn("Could not find ffmpeg in PATH! Falling back on mpv.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local file_duration = mp.get_property_native("duration")
|
||||||
|
local file_path = thumb_state.worker_input_path
|
||||||
|
|
||||||
|
if thumb_state.is_remote then
|
||||||
|
if (thumbnail_func == create_thumbnail_ffmpeg) then
|
||||||
|
msg.warn("Thumbnailing remote path, falling back on mpv.")
|
||||||
|
end
|
||||||
|
thumbnail_func = create_thumbnail_mpv
|
||||||
|
end
|
||||||
|
|
||||||
|
local generate_thumbnail_for_index = function(thumbnail_index)
|
||||||
|
-- Given a 1-based thumbnail index, generate a thumbnail for it based on the thumbnailer state
|
||||||
|
local thumb_idx = thumbnail_index - 1
|
||||||
|
msg.debug("Starting work on thumbnail", thumb_idx)
|
||||||
|
|
||||||
|
local thumbnail_path = thumb_state.thumbnail_template:format(thumb_idx)
|
||||||
|
-- Grab the "middle" of the thumbnail duration instead of the very start, and leave some margin in the end
|
||||||
|
local timestamp = math.min(file_duration - 0.25, (thumb_idx + 0.5) * thumb_state.thumbnail_delta)
|
||||||
|
|
||||||
|
mp.commandv("script-message", "mpv_thumbnail_script-progress", tostring(thumbnail_index))
|
||||||
|
|
||||||
|
-- The expected size (raw BGRA image)
|
||||||
|
local thumbnail_raw_size = (thumb_state.thumbnail_size.w * thumb_state.thumbnail_size.h * 4)
|
||||||
|
|
||||||
|
local need_thumbnail_generation = false
|
||||||
|
|
||||||
|
-- Check if the thumbnail already exists and is the correct size
|
||||||
|
local thumbnail_file = io.open(thumbnail_path, "rb")
|
||||||
|
if thumbnail_file == nil then
|
||||||
|
need_thumbnail_generation = true
|
||||||
|
else
|
||||||
|
local existing_thumbnail_filesize = thumbnail_file:seek("end")
|
||||||
|
if existing_thumbnail_filesize ~= thumbnail_raw_size then
|
||||||
|
-- Size doesn't match, so (re)generate
|
||||||
|
msg.warn("Thumbnail", thumb_idx, "did not match expected size, regenerating")
|
||||||
|
need_thumbnail_generation = true
|
||||||
|
end
|
||||||
|
thumbnail_file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
if need_thumbnail_generation then
|
||||||
|
local ret = thumbnail_func(file_path, timestamp, thumb_state.thumbnail_size, thumbnail_path, thumb_state.worker_extra)
|
||||||
|
local success = check_output(ret, thumbnail_path, thumbnail_func == create_thumbnail_mpv)
|
||||||
|
|
||||||
|
if success == nil then
|
||||||
|
-- Killed by us, changing files, ignore
|
||||||
|
msg.debug("Changing files, subprocess killed")
|
||||||
|
return true
|
||||||
|
elseif not success then
|
||||||
|
-- Real failure
|
||||||
|
mp.osd_message("Thumbnailing failed, check console for details", 3.5)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
msg.debug("Thumbnail", thumb_idx, "already done!")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Verify thumbnail size
|
||||||
|
-- Sometimes ffmpeg will output an empty file when seeking to a "bad" section (usually the end)
|
||||||
|
thumbnail_file = io.open(thumbnail_path, "rb")
|
||||||
|
|
||||||
|
-- Bail if we can't read the file (it should really exist by now, we checked this in check_output!)
|
||||||
|
if thumbnail_file == nil then
|
||||||
|
msg.error("Thumbnail suddenly disappeared!")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check the size of the generated file
|
||||||
|
local thumbnail_file_size = thumbnail_file:seek("end")
|
||||||
|
thumbnail_file:close()
|
||||||
|
|
||||||
|
-- Check if the file is big enough
|
||||||
|
local missing_bytes = math.max(0, thumbnail_raw_size - thumbnail_file_size)
|
||||||
|
if missing_bytes > 0 then
|
||||||
|
msg.warn(("Thumbnail missing %d bytes (expected %d, had %d), padding %s"):format(
|
||||||
|
missing_bytes, thumbnail_raw_size, thumbnail_file_size, thumbnail_path
|
||||||
|
))
|
||||||
|
-- Pad the file if it's missing content (eg. ffmpeg seek to file end)
|
||||||
|
thumbnail_file = io.open(thumbnail_path, "ab")
|
||||||
|
thumbnail_file:write(string.rep(string.char(0), missing_bytes))
|
||||||
|
thumbnail_file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.debug("Finished work on thumbnail", thumb_idx)
|
||||||
|
mp.commandv("script-message", "mpv_thumbnail_script-ready", tostring(thumbnail_index), thumbnail_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
msg.debug(("Generating %d thumbnails @ %dx%d for %q"):format(
|
||||||
|
#thumbnail_indexes,
|
||||||
|
thumb_state.thumbnail_size.w,
|
||||||
|
thumb_state.thumbnail_size.h,
|
||||||
|
file_path))
|
||||||
|
|
||||||
|
for i, thumbnail_index in ipairs(thumbnail_indexes) do
|
||||||
|
local bail = generate_thumbnail_for_index(thumbnail_index)
|
||||||
|
if bail then return end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set up listeners and keybinds
|
||||||
|
|
||||||
|
-- Job listener
|
||||||
|
mp.register_script_message("mpv_thumbnail_script-job", do_worker_job)
|
||||||
|
|
||||||
|
|
||||||
|
-- Register this worker with the master script
|
||||||
|
local register_timer = nil
|
||||||
|
local register_timeout = mp.get_time() + 1.5
|
||||||
|
|
||||||
|
local register_function = function()
|
||||||
|
if mp.get_time() > register_timeout and register_timer then
|
||||||
|
msg.error("Thumbnail worker registering timed out")
|
||||||
|
register_timer:stop()
|
||||||
|
else
|
||||||
|
msg.debug("Announcing self to master...")
|
||||||
|
mp.commandv("script-message", "mpv_thumbnail_script-worker", mp.get_script_name())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
register_timer = mp.add_periodic_timer(0.1, register_function)
|
||||||
|
|
||||||
|
mp.register_script_message("mpv_thumbnail_script-slaved", function()
|
||||||
|
msg.debug("Successfully registered with master")
|
||||||
|
register_timer:stop()
|
||||||
|
end)
|
||||||
11
config/mpv/scripts/pause-indicator.lua
Normal file
11
config/mpv/scripts/pause-indicator.lua
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
local ov = mp.create_osd_overlay('ass-events')
|
||||||
|
ov.data = [[{\an5\p1\alpha&H79\1c&Hffffff&\3a&Hff\pos(760,440)}]] ..
|
||||||
|
[[m-125 -75 l 2 2 l -125 75]]
|
||||||
|
|
||||||
|
mp.observe_property('pause', 'bool', function(_, paused)
|
||||||
|
mp.add_timeout(0.1, function()
|
||||||
|
if paused then ov:update()
|
||||||
|
else ov:remove() end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
657
config/mpv/tech-overview.txt
Normal file
657
config/mpv/tech-overview.txt
Normal file
|
|
@ -0,0 +1,657 @@
|
||||||
|
This file intends to give a big picture overview of how mpv is structured.
|
||||||
|
|
||||||
|
player/*.c:
|
||||||
|
Essentially makes up the player applications, including the main() function
|
||||||
|
and the playback loop.
|
||||||
|
|
||||||
|
Generally, it accesses all other subsystems, initializes them, and pushes
|
||||||
|
data between them during playback.
|
||||||
|
|
||||||
|
The structure is as follows (as of commit e13c05366557cb):
|
||||||
|
* main():
|
||||||
|
* basic initializations (e.g. init_libav() and more)
|
||||||
|
* pre-parse command line (verbosity level, config file locations)
|
||||||
|
* load config files (parse_cfgfiles())
|
||||||
|
* parse command line, add files from the command line to playlist
|
||||||
|
(m_config_parse_mp_command_line())
|
||||||
|
* check help options etc. (call handle_help_options()), possibly exit
|
||||||
|
* call mp_play_files() function that works down the playlist:
|
||||||
|
* run idle loop (idle_loop()), until there are files in the
|
||||||
|
playlist or an exit command was given (only if --idle it set)
|
||||||
|
* actually load and play a file in play_current_file():
|
||||||
|
* run all the dozens of functions to load the file and
|
||||||
|
initialize playback
|
||||||
|
* run a small loop that does normal playback, until the file is
|
||||||
|
done or a command terminates playback
|
||||||
|
(on each iteration, run_playloop() is called, which is rather
|
||||||
|
big and complicated - it decodes some audio and video on
|
||||||
|
each frame, waits for input, etc.)
|
||||||
|
* uninitialize playback
|
||||||
|
* determine next entry on the playlist to play
|
||||||
|
* loop, or exit if no next file or quit is requested
|
||||||
|
(see enum stop_play_reason)
|
||||||
|
* call mp_destroy()
|
||||||
|
* run_playloop():
|
||||||
|
* calls fill_audio_out_buffers()
|
||||||
|
This checks whether new audio needs to be decoded, and pushes it
|
||||||
|
to the AO.
|
||||||
|
* calls write_video()
|
||||||
|
Decode new video, and push it to the VO.
|
||||||
|
* determines whether playback of the current file has ended
|
||||||
|
* determines when to start playback after seeks
|
||||||
|
* and calls a whole lot of other stuff
|
||||||
|
(Really, this function does everything.)
|
||||||
|
|
||||||
|
Things worth saying about the playback core:
|
||||||
|
- most state is in MPContext (core.h), which is not available to the
|
||||||
|
subsystems (and should not be made available)
|
||||||
|
- the currently played tracks are in mpctx->current_tracks, and decoder
|
||||||
|
state in track.dec/d_sub
|
||||||
|
- the other subsystems rarely call back into the frontend, and the frontend
|
||||||
|
polls them instead (probably a good thing)
|
||||||
|
- one exceptions are wakeup callbacks, which notify a "higher" component
|
||||||
|
of a changed situation in a subsystem
|
||||||
|
|
||||||
|
I like to call the player/*.c files the "frontend".
|
||||||
|
|
||||||
|
ta.h & ta.c:
|
||||||
|
Hierarchical memory manager inspired by talloc from Samba. It's like a
|
||||||
|
malloc() with more features. Most importantly, each talloc allocation can
|
||||||
|
have a parent, and if the parent is free'd, all children will be free'd as
|
||||||
|
well. The parent is an arbitrary talloc allocation. It's either set by the
|
||||||
|
allocation call by passing a talloc parent, usually as first argument to the
|
||||||
|
allocation function. It can also be set or reset later by other calls (at
|
||||||
|
least talloc_steal()). A talloc allocation that is used as parent is often
|
||||||
|
called a talloc context.
|
||||||
|
|
||||||
|
One very useful feature of talloc is fast tracking of memory leaks. ("Fast"
|
||||||
|
as in it doesn't require valgrind.) You can enable it by setting the
|
||||||
|
MPV_LEAK_REPORT environment variable to "1":
|
||||||
|
export MPV_LEAK_REPORT=1
|
||||||
|
Or permanently by building with --enable-ta-leak-report.
|
||||||
|
This will list all unfree'd allocations on exit.
|
||||||
|
|
||||||
|
Documentation can be found here:
|
||||||
|
http://git.samba.org/?p=samba.git;a=blob;f=lib/talloc/talloc.h;hb=HEAD
|
||||||
|
|
||||||
|
For some reason, we're still using API-compatible wrappers instead of TA
|
||||||
|
directly. The talloc wrapper has only a subset of the functionality, and
|
||||||
|
in particular the wrappers abort() on memory allocation failure.
|
||||||
|
|
||||||
|
Note: unlike tcmalloc, jemalloc, etc., talloc() is not actually a malloc
|
||||||
|
replacement. It works on top of system malloc and provides additional
|
||||||
|
features that are supposed to make memory management easier.
|
||||||
|
|
||||||
|
player/command.c:
|
||||||
|
This contains the implementation for client API commands and properties.
|
||||||
|
Properties are essentially dynamic variables changed by certain commands.
|
||||||
|
This is basically responsible for all user commands, like initiating
|
||||||
|
seeking, switching tracks, etc. It calls into other player/*.c files,
|
||||||
|
where most of the work is done, but also calls other parts of mpv.
|
||||||
|
|
||||||
|
player/core.h:
|
||||||
|
Data structures and function prototypes for most of player/*.c. They are
|
||||||
|
usually not accessed by other parts of mpv for the sake of modularization.
|
||||||
|
|
||||||
|
player/client.c:
|
||||||
|
This implements the client API (libmpv/client.h). For the most part, this
|
||||||
|
just calls into other parts of the player. This also manages a ringbuffer
|
||||||
|
of events from player to clients.
|
||||||
|
|
||||||
|
options/options.h, options/options.c
|
||||||
|
options.h contains the global option struct MPOpts. The option declarations
|
||||||
|
(option names, types, and MPOpts offsets for the option parser) are in
|
||||||
|
options.c. Most default values for options and MPOpts are in
|
||||||
|
mp_default_opts at the end of options.c.
|
||||||
|
|
||||||
|
MPOpts is unfortunately quite monolithic, but is being incrementally broken
|
||||||
|
up into sub-structs. Many components have their own sub-option structs
|
||||||
|
separate from MPOpts. New options should be bound to the component that uses
|
||||||
|
them. Add a new option table/struct if needed.
|
||||||
|
|
||||||
|
The global MPOpts still contains the sub-structs as fields, which serves to
|
||||||
|
link them to the option parser. For example, an entry like this may be
|
||||||
|
typical:
|
||||||
|
|
||||||
|
{"", OPT_SUBSTRUCT(demux_opts, demux_conf)},
|
||||||
|
|
||||||
|
This directs the option access code to include all options in demux_conf
|
||||||
|
into the global option list, with no prefix (""), and as part of the
|
||||||
|
MPOpts.demux_opts field. The MPOpts.demux_opts field is actually not
|
||||||
|
accessed anywhere, and instead demux.c does this:
|
||||||
|
|
||||||
|
struct m_config_cache *opts_cache =
|
||||||
|
m_config_cache_alloc(demuxer, global, &demux_conf);
|
||||||
|
struct demux_opts *opts = opts_cache->opts;
|
||||||
|
|
||||||
|
... to get a copy of its options.
|
||||||
|
|
||||||
|
See m_config.h (below) how to access options.
|
||||||
|
|
||||||
|
The actual option parser is spread over m_option.c, m_config.c, and
|
||||||
|
parse_commandline.c, and uses the option table in options.c.
|
||||||
|
|
||||||
|
options/m_config.h & m_config.c:
|
||||||
|
Code for querying and managing options. This (unfortunately) contains both
|
||||||
|
declarations for the "legacy-ish" global m_config struct, and ways to access
|
||||||
|
options in a threads-safe way anywhere, like m_config_cache_alloc().
|
||||||
|
|
||||||
|
m_config_cache_alloc() lets anyone read, observe, and write options in any
|
||||||
|
thread. The only state it needs is struct mpv_global, which is an opaque
|
||||||
|
type that can be passed "down" the component hierarchy. For safety reasons,
|
||||||
|
you should not pass down any pointers to option structs (like MPOpts), but
|
||||||
|
instead pass down mpv_global, and use m_config_cache_alloc() (or similar)
|
||||||
|
to get a synchronized copy of the options.
|
||||||
|
|
||||||
|
input/input.c:
|
||||||
|
This translates keyboard input coming from VOs and other sources (such
|
||||||
|
as remote control devices like Apple IR or client API commands) to the
|
||||||
|
key bindings listed in the user's (or the builtin) input.conf and turns
|
||||||
|
them into items of type struct mp_cmd. These commands are queued, and read
|
||||||
|
by playloop.c. They get pushed with run_command() to command.c.
|
||||||
|
|
||||||
|
Note that keyboard input and commands used by the client API are the same.
|
||||||
|
The client API only uses the command parser though, and has its own queue
|
||||||
|
of input commands somewhere else.
|
||||||
|
|
||||||
|
common/msg.h:
|
||||||
|
All terminal output must go through mp_msg().
|
||||||
|
|
||||||
|
stream/*:
|
||||||
|
File input is implemented here. stream.h/.c provides a simple stream based
|
||||||
|
interface (like reading a number of bytes at a given offset). mpv can
|
||||||
|
also play from http streams and such, which is implemented here.
|
||||||
|
|
||||||
|
E.g. if mpv sees "http://something" on the command line, it will pick
|
||||||
|
stream_lavf.c based on the prefix, and pass the rest of the filename to it.
|
||||||
|
|
||||||
|
Some stream inputs are quite special: stream_dvd.c turns DVDs into mpeg
|
||||||
|
streams (DVDs are actually a bunch of vob files etc. on a filesystem),
|
||||||
|
stream_tv.c provides TV input including channel switching.
|
||||||
|
|
||||||
|
Some stream inputs are just there to invoke special demuxers, like
|
||||||
|
stream_mf.c. (Basically to make the prefix "mf://" do something special.)
|
||||||
|
|
||||||
|
demux/:
|
||||||
|
Demuxers split data streams into audio/video/sub streams, which in turn
|
||||||
|
are split in packets. Packets (see demux_packet.h) are mostly byte chunks
|
||||||
|
tagged with a playback time (PTS). These packets are passed to the decoders.
|
||||||
|
|
||||||
|
Most demuxers have been removed from this fork, and the only important and
|
||||||
|
"actual" demuxers left are demux_mkv.c and demux_lavf.c (uses libavformat).
|
||||||
|
There are some pseudo demuxers like demux_cue.c.
|
||||||
|
|
||||||
|
The main interface is in demux.h. The stream headers are in stheader.h.
|
||||||
|
There is a stream header for each audio/video/sub stream, and each of them
|
||||||
|
holds codec information about the stream and other information.
|
||||||
|
|
||||||
|
demux.c is a bit big, the main reason being that it contains the demuxer
|
||||||
|
cache, which is implemented as a list of packets. The cache is complex
|
||||||
|
because it support seeking, multiple ranges, prefetching, and so on.
|
||||||
|
|
||||||
|
video/:
|
||||||
|
This contains several things related to audio/video decoding, as well as
|
||||||
|
video filters.
|
||||||
|
|
||||||
|
mp_image.h and img_format.h define how mpv stores decoded video frames
|
||||||
|
internally.
|
||||||
|
|
||||||
|
video/decode/:
|
||||||
|
vd_*.c are video decoders. (There's only vd_lavc.c left.) dec_video.c
|
||||||
|
handles most of connecting the frontend with the actual decoder.
|
||||||
|
|
||||||
|
video/filter/:
|
||||||
|
vf_*.c and vf.c form the video filter chain. They are fed by the video
|
||||||
|
decoder, and output the filtered images to the VOs though vf_vo.c. By
|
||||||
|
default, no video filters (except vf_vo) are used. vf_scale is automatically
|
||||||
|
inserted if the video output can't handle the video format used by the
|
||||||
|
decoder.
|
||||||
|
|
||||||
|
video/out/:
|
||||||
|
Video output. They also create GUI windows and handle user input. In most
|
||||||
|
cases, the windowing code is shared among VOs, like x11_common.c for X11 and
|
||||||
|
w32_common.c for Windows. The VOs stand between frontend and windowing code.
|
||||||
|
vo_gpu can pick a windowing system at runtime, e.g. the same binary can
|
||||||
|
provide both X11 and Cocoa support on OSX.
|
||||||
|
|
||||||
|
VOs can be reconfigured at runtime. A vo_reconfig() call can change the video
|
||||||
|
resolution and format, without destroying the window.
|
||||||
|
|
||||||
|
vo_gpu should be taken as reference.
|
||||||
|
|
||||||
|
audio/:
|
||||||
|
format.h/format.c define the uncompressed audio formats. (As well as some
|
||||||
|
compressed formats used for spdif.)
|
||||||
|
|
||||||
|
audio/decode/:
|
||||||
|
ad_*.c and dec_audio.c handle audio decoding. ad_lavc.c is the
|
||||||
|
decoder using ffmpeg. ad_spdif.c is not really a decoder, but is used for
|
||||||
|
compressed audio passthrough.
|
||||||
|
|
||||||
|
audio/filter/:
|
||||||
|
Audio filter chain. af_lavrresample is inserted if any form of conversion
|
||||||
|
between audio formats is needed.
|
||||||
|
|
||||||
|
audio/out/:
|
||||||
|
Audio outputs.
|
||||||
|
|
||||||
|
Unlike VOs, AOs can't be reconfigured on a format change. On audio format
|
||||||
|
changes, the AO will simply be closed and re-opened.
|
||||||
|
|
||||||
|
There are wrappers to support for two types of audio APIs: push.c and
|
||||||
|
pull.c. ao.c calls into one of these. They contain generic code to deal
|
||||||
|
with the data flow these APIs impose.
|
||||||
|
|
||||||
|
Note that mpv synchronizes the video to the audio. That's the reason
|
||||||
|
why buggy audio drivers can have a bad influence on playback quality.
|
||||||
|
|
||||||
|
sub/:
|
||||||
|
Contains subtitle and OSD rendering.
|
||||||
|
|
||||||
|
osd.c/.h is actually the OSD code. It queries dec_sub.c to retrieve
|
||||||
|
decoded/rendered subtitles. osd_libass.c is the actual implementation of
|
||||||
|
the OSD text renderer (which uses libass, and takes care of all the tricky
|
||||||
|
fontconfig/freetype API usage and text layouting).
|
||||||
|
|
||||||
|
The VOs call osd.c to render OSD and subtitle (via e.g. osd_draw()). osd.c
|
||||||
|
in turn asks dec_sub.c for subtitle overlay bitmaps, which relays the
|
||||||
|
request to one of the sd_*.c subtitle decoders/renderers.
|
||||||
|
|
||||||
|
Subtitle loading is in demux/. The MPlayer subreader.c is mostly gone - parts
|
||||||
|
of it survive in demux_subreader.c. It's used as last fallback, or to handle
|
||||||
|
some text subtitle types on Libav. It should go away eventually. Normally,
|
||||||
|
subtitles are loaded via demux_lavf.c.
|
||||||
|
|
||||||
|
The subtitles are passed to dec_sub.c and the subtitle decoders in sd_*.c
|
||||||
|
as they are demuxed. All text subtitles are rendered by sd_ass.c. If text
|
||||||
|
subtitles are not in the ASS format, the libavcodec subtitle converters are
|
||||||
|
used (lavc_conv.c).
|
||||||
|
|
||||||
|
Text subtitles can be preloaded, in which case they are read fully as soon
|
||||||
|
as the subtitle is selected. In this case, they are effectively stored in
|
||||||
|
sd_ass.c's internal state.
|
||||||
|
|
||||||
|
etc/:
|
||||||
|
The file input.conf is actually integrated into the mpv binary by the
|
||||||
|
build system. It contains the default keybindings.
|
||||||
|
|
||||||
|
Best practices and Concepts within mpv
|
||||||
|
======================================
|
||||||
|
|
||||||
|
General contribution etc.
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
See: DOCS/contribute.md
|
||||||
|
|
||||||
|
Error checking
|
||||||
|
--------------
|
||||||
|
|
||||||
|
If an error is relevant, it should be handled. If it's interesting, log the
|
||||||
|
error. However, mpv often keeps errors silent and reports failures somewhat
|
||||||
|
coarsely by propagating them upwards the caller chain. This is OK, as long as
|
||||||
|
the errors are not very interesting, or would require a developer to debug it
|
||||||
|
anyway (in which case using a debugger would be more convenient, and the
|
||||||
|
developer would need to add temporary debug printfs to get extremely detailed
|
||||||
|
information which would not be appropriate during normal operation).
|
||||||
|
|
||||||
|
Basically, keep a balance on error reporting. But always check them, unless you
|
||||||
|
have a good argument not to.
|
||||||
|
|
||||||
|
Memory allocation errors (OOM) are a special class of errors. Normally such
|
||||||
|
allocation failures are not handled "properly". Instead, abort() is called.
|
||||||
|
(New code should use MP_HANDLE_OOM() for this.) This is done out of laziness and
|
||||||
|
for convenience, and due to the fact that MPlayer/mplayer2 never handled it
|
||||||
|
correctly. (MPlayer varied between handling it correctly, trying to do so but
|
||||||
|
failing, and just not caring, while mplayer2 started using abort() for it.)
|
||||||
|
|
||||||
|
This is justifiable in a number of ways. Error handling paths are notoriously
|
||||||
|
untested and buggy, so merely having them won't make your program more reliable.
|
||||||
|
Having these error handling paths also complicates non-error code, due to the
|
||||||
|
need to roll back state at any point after a memory allocation.
|
||||||
|
|
||||||
|
Take any larger body of code, that is supposed to handle OOM, and test whether
|
||||||
|
the error paths actually work, for example by overriding malloc with a version
|
||||||
|
that randomly fails. You will find bugs quickly, and often they will be very
|
||||||
|
annoying to fix (if you can even reproduce them).
|
||||||
|
|
||||||
|
In addition, a clear indication that something went wrong may be missing. On
|
||||||
|
error your program may exhibit "degraded" behavior by design. Consider a video
|
||||||
|
encoder dropping frames somewhere in the middle of a video due to temporary
|
||||||
|
allocation failures, instead of just exiting with an errors. In other cases, it
|
||||||
|
may open conceptual security holes. Failing fast may be better.
|
||||||
|
|
||||||
|
mpv uses GPU APIs, which may be break on allocation errors (because driver
|
||||||
|
authors will have the same issues as described here), or don't even have a real
|
||||||
|
concept for dealing with OOM (OpenGL).
|
||||||
|
|
||||||
|
libmpv is often used by GUIs, which I predict always break if OOM happens.
|
||||||
|
|
||||||
|
Last but not least, OSes like Linux use "overcommit", which basically means that
|
||||||
|
your program may crash any time OOM happens, even if it doesn't use malloc() at
|
||||||
|
all!
|
||||||
|
|
||||||
|
But still, don't just assume malloc() always succeeds. Use MP_HANDLE_OOM(). The
|
||||||
|
ta* APIs do this for you. The reason for this is that dereferencing a NULL
|
||||||
|
pointer can have security relevant consequences if large offsets are involved.
|
||||||
|
Also, a clear error message is better than a random segfault.
|
||||||
|
|
||||||
|
Some big memory allocations are checked anyway. For example, all code must
|
||||||
|
assume that allocating video frames or packets can fail. (The above example
|
||||||
|
of dropping video frames during encoding is entirely possible in mpv.)
|
||||||
|
|
||||||
|
Undefined behavior
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Undefined behavior (UB) is a concept in the C language. C is famous for being a
|
||||||
|
language that makes it almost impossible to write working code, because
|
||||||
|
undefined behavior is so easily triggered, compilers will happily abuse it to
|
||||||
|
generate "faster" code, debugging tools will shout at you, and sometimes it
|
||||||
|
even means your code doesn't work.
|
||||||
|
|
||||||
|
There is a lot of literature on this topic. Read it.
|
||||||
|
|
||||||
|
(In C's defense, UB exists in other languages too, but since they're not used
|
||||||
|
for low level infrastructure, and/or these languages are at times not rigorously
|
||||||
|
defined, simply nobody cares. However, the C standard committee is still guilty
|
||||||
|
for not addressing this. I'll admit that I can't even tell from the standard's
|
||||||
|
gibberish whether some specific behavior is UB or not. It's written like tax
|
||||||
|
law.)
|
||||||
|
|
||||||
|
In mpv, we generally try to avoid undefined behavior. For one, we want portable
|
||||||
|
and reliable operation. But more importantly, we want clean output from
|
||||||
|
debugging tools, in order to find real bugs more quickly and effectively.
|
||||||
|
|
||||||
|
Avoid the "works in practice" argument. Once debugging tools come into play, or
|
||||||
|
simply when "in practice" stops being true, this will all get back to you in a
|
||||||
|
bad way.
|
||||||
|
|
||||||
|
Global state, library safety
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Mutable global state is when code uses global variables that are not read-only.
|
||||||
|
This must be avoided in mpv. Always use context structs that the caller of
|
||||||
|
your code needs to allocate, and whose pointers are passed to your functions.
|
||||||
|
|
||||||
|
Library safety means that your code (or library) can be used by a library
|
||||||
|
without causing conflicts with other library users in the same process. To any
|
||||||
|
piece of code, a "safe" library's API can simply be used, without having to
|
||||||
|
worry about other API users that may be around somewhere.
|
||||||
|
|
||||||
|
Libraries are often not library safe, because they they use global mutable state
|
||||||
|
or other "global" resources. Typical examples include use of signals, simple
|
||||||
|
global variables (like hsearch() in libc), or internal caches not protected by
|
||||||
|
locks.
|
||||||
|
|
||||||
|
A surprisingly high number of libraries are not library safe because they need
|
||||||
|
global initialization. Typically they provide an API function, which
|
||||||
|
"initializes" the library, and which must be called before calling any other
|
||||||
|
API functions. Often, you are to provide global configuration parameters, which
|
||||||
|
can change the behavior of the library. If two libraries A and B use library C,
|
||||||
|
but A and B initialize C with different parameters, something "bad" may happen.
|
||||||
|
In addition, these global initialization functions are often not thread-safe. So
|
||||||
|
if A and B try to initialize C at the same time (from different threads and
|
||||||
|
without knowing about each other), it may cause undefined behavior. (libcurl is
|
||||||
|
a good example of both of these issues. FFmpeg and some TLS libraries used to be
|
||||||
|
affected, but improved.)
|
||||||
|
|
||||||
|
This is so bad because library A and B from the previous example most likely
|
||||||
|
have no way to cooperate, because they're from different authors and have no
|
||||||
|
business knowing each others. They'd need a library D, which wraps library C
|
||||||
|
in a safe way. Unfortunately, typically something worse happens: libraries get
|
||||||
|
"infected" by the unsafeness of its sub-libraries, and export a global init API
|
||||||
|
just to initialize the sub-libraries. In the previous example, libraries A and B
|
||||||
|
would export global init APIs just to init library C, even though the rest of
|
||||||
|
A/B are clean and library safe. (Again, libcurl is an example of this, if you
|
||||||
|
subtract other historic anti-features.)
|
||||||
|
|
||||||
|
The main problem with library safety is that its lack propagates to all
|
||||||
|
libraries using the library.
|
||||||
|
|
||||||
|
We require libmpv to be library safe. This is not really possible, because some
|
||||||
|
libraries are not library safe (FFmpeg, Xlib, partially ALSA). However, for
|
||||||
|
ideological reasons, there is no global init API, and best effort is made to try
|
||||||
|
to avoid problems.
|
||||||
|
|
||||||
|
libmpv has some features that are not library safe, but which are disabled by
|
||||||
|
default (such as terminal usage aka stdout, or JSON IPC blocking SIGPIPE for
|
||||||
|
internal convenience).
|
||||||
|
|
||||||
|
A notable, very disgustingly library unsafe behavior of libmpv is calling
|
||||||
|
abort() on some memory allocation failure. See error checking section.
|
||||||
|
|
||||||
|
Logging
|
||||||
|
-------
|
||||||
|
|
||||||
|
All logging and terminal output in mpv goes through the functions and macros
|
||||||
|
provided in common/msg.h. This is in part for library safety, and in part to
|
||||||
|
make sure users can silence all output, or to redirect the output elsewhere,
|
||||||
|
like a log file or the internal console.lua script.
|
||||||
|
|
||||||
|
Locking
|
||||||
|
-------
|
||||||
|
|
||||||
|
See generally available literature. In mpv, we use pthread for this.
|
||||||
|
|
||||||
|
Always keep locking clean. Don't skip locking just because it will work "in
|
||||||
|
practice". (See undefined behavior section.) If your use case is simple, you may
|
||||||
|
use C11 atomics (osdep/atomic.h for partial C99 support), but most likely you
|
||||||
|
will only hurt yourself and others.
|
||||||
|
|
||||||
|
Always make clear which fields in a struct are protected by which lock. If a
|
||||||
|
field is immutable, or simply not thread-safe (e.g. state for a single worker
|
||||||
|
thread), document it as well.
|
||||||
|
|
||||||
|
Internal mpv APIs are assumed to be not thread-safe by default. If they have
|
||||||
|
special guarantees (such as being usable by more than one thread at a time),
|
||||||
|
these should be explicitly documented.
|
||||||
|
|
||||||
|
All internal mpv APIs must be free of global state. Even if a component is not
|
||||||
|
thread-safe, multiple threads can use _different_ instances of it without any
|
||||||
|
locking.
|
||||||
|
|
||||||
|
On a side note, recursive locks may seem convenient at first, but introduce
|
||||||
|
additional problems with condition variables and locking hierarchies. They
|
||||||
|
should be avoided.
|
||||||
|
|
||||||
|
Locking hierarchy
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
A simple way to avoid deadlocks with classic locking is to define a locking
|
||||||
|
hierarchy or lock order. If all threads acquire locks in the same order, no
|
||||||
|
deadlocks will happen.
|
||||||
|
|
||||||
|
For example, a "leaf" lock is a lock that is below all other locks in the
|
||||||
|
hierarchy. You can acquire it any time, as long as you don't acquire other
|
||||||
|
locks while holding it.
|
||||||
|
|
||||||
|
Unfortunately, C has no way to declare or check the lock order, so you should at
|
||||||
|
least document it.
|
||||||
|
|
||||||
|
In addition, try to avoid exposing locks to the outside. Making the declaration
|
||||||
|
of a lock private to a specific .c file (and _not_ exporting accessors or
|
||||||
|
lock/unlock functions that manipulate the lock) is a good idea. Your component's
|
||||||
|
API may acquire internal locks, but should release them when returning. Keeping
|
||||||
|
the entire locking in a single file makes it easy to check it.
|
||||||
|
|
||||||
|
Avoiding callback hell
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
mpv code is separated in components, like the "frontend" (i.e. MPContext mpctx),
|
||||||
|
VOs, AOs, demuxers, and more. The frontend usually calls "down" the usage
|
||||||
|
hierarchy: mpctx almost on top, then things like vo/ao, and utility code on the
|
||||||
|
very bottom.
|
||||||
|
|
||||||
|
"Callback hell" is when when components call both up and down the hierarchy,
|
||||||
|
which for example leads to accidentally recursion, reentrancy problems, or
|
||||||
|
locking nightmares. This is avoided by (mostly) calling only down the hierarchy.
|
||||||
|
Basically the call graph forms a DAG. The other direction is handled by event
|
||||||
|
queues, wakeup callbacks, and similar mechanisms.
|
||||||
|
|
||||||
|
Typically, a component provides an API, and does not know anything about its
|
||||||
|
user. The API user (component higher in the hierarchy) polls the state of the
|
||||||
|
lower component when needed.
|
||||||
|
|
||||||
|
This also enforces some level of modularization, and with some luck the locking
|
||||||
|
hierarchy. (Basically, locks of lower components automatically become leaf
|
||||||
|
locks.) Another positive effect is simpler memory management.
|
||||||
|
|
||||||
|
(Also see e.g.: http://250bpm.com/blog:24)
|
||||||
|
|
||||||
|
Wakeup callbacks
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This is a common concept in mpv. Even the public API uses it. It's used when an
|
||||||
|
API has internal threads (or otherwise triggers asynchronous events), but the
|
||||||
|
component call hierarchy needs to be kept. The wakeup callback is the only
|
||||||
|
exception to the call hierarchy, and always calls up.
|
||||||
|
|
||||||
|
For example, vo spawns a thread that the API user (the mpv frontend) does not
|
||||||
|
need to know about. vo simply provides a single-threaded API (or that looks like
|
||||||
|
one). This API needs a way to notify the API user of new events. But the vo
|
||||||
|
event producer is on the vo thread - it can't simply invoke a callback back into
|
||||||
|
the API user, because then the API user has to deal with locking, despite not
|
||||||
|
using threads. In addition, this will probably cause problems like mentioned in
|
||||||
|
the "callback hell" section, especially lock order issues.
|
||||||
|
|
||||||
|
The solution is the wakeup callback. It merely unblocks the API user from
|
||||||
|
waiting, and the API user then uses the normal vo API to examine whether or
|
||||||
|
which state changed. As a concept, it documents what a wakeup callback is
|
||||||
|
allowed to do and what not, to avoid the aforementioned problems.
|
||||||
|
|
||||||
|
Generally, you are not allowed to call any API from the wakeup callback. You
|
||||||
|
just do whatever is needed to unblock your thread. For example, if it's waiting
|
||||||
|
on a mutex/condition variable, acquire the mutex, set a change flag, signal
|
||||||
|
the condition variable, unlock, return. (This mutex must not be held when
|
||||||
|
calling the API. It must be a leaf lock.)
|
||||||
|
|
||||||
|
Restricting the wakeup callback like this sidesteps any reentrancy issues and
|
||||||
|
other complexities. The API implementation can simply hold internal (and
|
||||||
|
non-recursive) locks while invoking the wakeup callback.
|
||||||
|
|
||||||
|
The API user still needs to deal with locking (probably), but there's only the
|
||||||
|
need to implement a single "receiver", that can handle the entire API of the
|
||||||
|
used component. (Or multiple APIs - MPContext for example has only 1 wakeup
|
||||||
|
callback that handles all AOs, VOs, input, demuxers, and more. It simple re-runs
|
||||||
|
the playloop.)
|
||||||
|
|
||||||
|
You could get something more advanced by turning this into a message queue. The
|
||||||
|
API would append a message to the queue, and the API user can read it. But then
|
||||||
|
you still need a way to "wakeup" the API user (unless you force the API user
|
||||||
|
to block on your API, which will make things inconvenient for the API user). You
|
||||||
|
also need to worry about what happens if the message queue overruns (you either
|
||||||
|
lose messages or have unbounded memory usage). In the mpv public API, the
|
||||||
|
distinction between message queue and wakeup callback is sort of blurry, because
|
||||||
|
it does provide a message queue, but an additional wakeup callback, so API
|
||||||
|
users are not required to call mpv_wait_event() with a high timeout.
|
||||||
|
|
||||||
|
mpv itself prefers using wakeup callbacks over a generic event queue, because
|
||||||
|
most times an event queue is not needed (or complicates things), and it is
|
||||||
|
better to do it manually.
|
||||||
|
|
||||||
|
(You could still abstract the API user side of wakeup callback handling, and
|
||||||
|
avoid reimplementing it all the time. Although mp_dispatch_queue already
|
||||||
|
provides mechanisms for this.)
|
||||||
|
|
||||||
|
Condition variables
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
They're used whenever a thread needs to wait for something, without nonsense
|
||||||
|
like sleep calls or busy waiting. mpv uses the standard pthread API for this.
|
||||||
|
There's a lot of literature on it. Read it.
|
||||||
|
|
||||||
|
For initial understanding, it may be helpful to know that condition variables
|
||||||
|
are not variables that signal a condition. pthread_cond_t does not have any
|
||||||
|
state per-se. Maybe pthread_cond_t would better be named pthread_interrupt_t,
|
||||||
|
because its sole purpose is to interrupt a thread waiting via pthread_cond_wait()
|
||||||
|
(or similar). The "something" in "waiting for something" can be called
|
||||||
|
predicate (to avoid confusing it with "condition"). Consult literature for the
|
||||||
|
proper terms.
|
||||||
|
|
||||||
|
The very short version is...
|
||||||
|
|
||||||
|
Shared declarations:
|
||||||
|
|
||||||
|
pthread_mutex_t lock;
|
||||||
|
pthread_cond_t cond_var;
|
||||||
|
struct something state_var; // protected by lock, changes signaled by cond_var
|
||||||
|
|
||||||
|
Waiter thread:
|
||||||
|
|
||||||
|
pthread_mutex_lock(&lock);
|
||||||
|
|
||||||
|
// Wait for a change in state_var. We want to wait until predicate_fulfilled()
|
||||||
|
// returns true.
|
||||||
|
// Must be a loop for 2 reasons:
|
||||||
|
// 1. cond_var may be associated with other conditions too
|
||||||
|
// 2. pthread_cond_wait() can have sporadic wakeups
|
||||||
|
while (!predicate_fulfilled(&state_var)) {
|
||||||
|
// This unlocks, waits for cond_var to be signaled, and then locks again.
|
||||||
|
// The _whole_ point of cond_var is that unlocking and waiting for the
|
||||||
|
// signal happens atomically.
|
||||||
|
pthread_cond_wait(&cond_var, &lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here you may react to the state change. The state cannot change
|
||||||
|
// asynchronously as long as you still hold the lock (and didn't release
|
||||||
|
// and reacquire it).
|
||||||
|
// ...
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&lock);
|
||||||
|
|
||||||
|
Signaler thread:
|
||||||
|
|
||||||
|
pthread_mutex_lock(&lock);
|
||||||
|
|
||||||
|
// Something changed. Update the shared variable with the new state.
|
||||||
|
update_state(&state_var);
|
||||||
|
|
||||||
|
// Notify that something changed. This will wake up the waiter thread if
|
||||||
|
// it's blocked in pthread_cond_wait(). If not, nothing happens.
|
||||||
|
pthread_cond_broadcast(&cond_var);
|
||||||
|
|
||||||
|
// Fun fact: good implementations wake up the waiter only when the lock is
|
||||||
|
// released, to reduce kernel scheduling overhead.
|
||||||
|
pthread_mutex_unlock(&lock);
|
||||||
|
|
||||||
|
Some basic rules:
|
||||||
|
1. Always access your state under proper locking
|
||||||
|
2. Always check your predicate before every call to pthread_cond_wait()
|
||||||
|
(And don't call pthread_cond_wait() if the predicate is fulfilled.)
|
||||||
|
3. Always call pthread_cond_wait() in a loop
|
||||||
|
(And only if your predicate failed without releasing the lock..)
|
||||||
|
4. Always call pthread_cond_broadcast()/_signal() inside of its associated
|
||||||
|
lock
|
||||||
|
|
||||||
|
mpv sometimes violates rule 3, and leaves "retrying" (i.e. looping) to the
|
||||||
|
caller.
|
||||||
|
|
||||||
|
Common pitfalls:
|
||||||
|
- Thinking that pthread_cond_t is some kind of semaphore, or holds any
|
||||||
|
application state or the user predicate (it _only_ wakes up threads
|
||||||
|
that are at the same time blocking on pthread_cond_wait() and friends,
|
||||||
|
nothing else)
|
||||||
|
- Changing the predicate, but not updating all pthread_cond_broadcast()/
|
||||||
|
_signal() calls correctly
|
||||||
|
- Forgetting that pthread_cond_wait() unlocks the lock (other threads can
|
||||||
|
and must acquire the lock)
|
||||||
|
- Holding multiple nested locks while trying to wait (=> deadlock, violates
|
||||||
|
the lock order anyway)
|
||||||
|
- Waiting for a predicate correctly, but unlocking/relocking before acting
|
||||||
|
on it (unlocking allows arbitrary state changes)
|
||||||
|
- Confusing which lock/condition var. is used to manage a bit of state
|
||||||
|
|
||||||
|
Generally available literature probably has better examples and explanations.
|
||||||
|
|
||||||
|
Using condition variables the proper way is generally preferred over using more
|
||||||
|
messy variants of them. (Just saying because on win32, "SetEvent" exists, and
|
||||||
|
it's inferior to condition variables. Try to avoid the win32 primitives, even if
|
||||||
|
you're dealing with Windows-only code.)
|
||||||
|
|
||||||
|
Threads
|
||||||
|
-------
|
||||||
|
|
||||||
|
Threading should be conservatively used. Normally, mpv code pretends to be
|
||||||
|
single-threaded, and provides thread-unsafe APIs. Threads are used coarsely,
|
||||||
|
and if you can avoid messing with threads, you should. For example, VOs and AOs
|
||||||
|
do not need to deal with threads normally, even though they run on separate
|
||||||
|
threads. The glue code "isolates" them from any threading issues.
|
||||||
|
|
@ -15,6 +15,10 @@ alias \
|
||||||
rm='rm -iv' \
|
rm='rm -iv' \
|
||||||
md='mkdir -pv';
|
md='mkdir -pv';
|
||||||
|
|
||||||
|
# short long and common commands
|
||||||
|
alias \
|
||||||
|
mkexec='chmod +x';
|
||||||
|
|
||||||
# Exa for listing
|
# Exa for listing
|
||||||
alias \
|
alias \
|
||||||
ls='exa -lh --color=always --icons --git ' \
|
ls='exa -lh --color=always --icons --git ' \
|
||||||
22
config/zsh/aliases/flatpak.zsh
Normal file
22
config/zsh/aliases/flatpak.zsh
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
# Auto-generated aliases for Flatpak applications
|
||||||
|
|
||||||
|
alias junction='flatpak run re.sonny.Junction'
|
||||||
|
alias slack='flatpak run com.slack.Slack'
|
||||||
|
alias flatseal='flatpak run com.github.tchx84.Flatseal'
|
||||||
|
alias kid3='flatpak run org.kde.kid3'
|
||||||
|
alias client='flatpak run com.spotify.Client'
|
||||||
|
alias gdmsettings='flatpak run io.github.realmazharhussain.GdmSettings'
|
||||||
|
alias studio='flatpak run io.beekeeperstudio.Studio'
|
||||||
|
alias librewolf-community='flatpak run io.gitlab.librewolf-community'
|
||||||
|
alias czkawka='flatpak run com.github.qarmin.czkawka'
|
||||||
|
alias detwinner='flatpak run com.neatdecisions.Detwinner'
|
||||||
|
alias syncthingtk='flatpak run me.kozec.syncthingtk'
|
||||||
|
alias jellyfin-media-player='flatpak run com.github.iwalton3.jellyfin-media-player'
|
||||||
|
alias guiscrcpy='flatpak run in.srev.guiscrcpy'
|
||||||
|
alias browser='flatpak run com.brave.Browser'
|
||||||
|
alias amberol='flatpak run io.bassi.Amberol'
|
||||||
|
alias signal='flatpak run org.signal.Signal'
|
||||||
|
alias pikabackup='flatpak run org.gnome.World.PikaBackup'
|
||||||
|
alias megasync='flatpak run nz.mega.MEGAsync'
|
||||||
|
alias celeste='flatpak run com.hunterwittenborn.Celeste'
|
||||||
2
config/zsh/aliases/fzf.zsh
Normal file
2
config/zsh/aliases/fzf.zsh
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
alias \
|
||||||
|
fzf-fp='fzf-flatpak-install-widget';
|
||||||
|
|
@ -2,7 +2,7 @@ HISTFILE=~/.histfile
|
||||||
HISTSIZE=1000
|
HISTSIZE=1000
|
||||||
SAVEHIST=1000
|
SAVEHIST=1000
|
||||||
setopt BANG_HIST
|
setopt BANG_HIST
|
||||||
setopt EXTENDED_HISTORY
|
setopt EXTENDED_HISTORY
|
||||||
setopt HIST_EXPIRE_DUPS_FIRST
|
setopt HIST_EXPIRE_DUPS_FIRST
|
||||||
setopt HIST_IGNORE_DUPS
|
setopt HIST_IGNORE_DUPS
|
||||||
setopt HIST_FIND_NO_DUPS
|
setopt HIST_FIND_NO_DUPS
|
||||||
|
|
|
||||||
89
config/zsh/functions/flatpak.zsh
Normal file
89
config/zsh/functions/flatpak.zsh
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
export FLATPAK_ALIAS_FILE="${DOTS:-$HOME/.config}/config/zsh/aliases/flatpak.zsh"
|
||||||
|
export FLATPAK_EXPORT_FILE="${DOTS:-$HOME/Desktop}/exports/flatpak-apps.txt"
|
||||||
|
|
||||||
|
fp() {
|
||||||
|
set -e
|
||||||
|
generate_flatpak_alias() {
|
||||||
|
local flatpak_alias_file=${1:-$FLATPAK_ALIAS_FILE}
|
||||||
|
[[ -f $flatpak_alias_file ]] && touch "$flatpak_alias_file"
|
||||||
|
local flatpak_binaries_dir="/var/lib/flatpak/exports/bin"
|
||||||
|
if [[ -d "$flatpak_binaries_dir" ]]; then
|
||||||
|
echo -e "\n# Auto-generated aliases for Flatpak applications\n" >> "${flatpak_alias_file}"
|
||||||
|
|
||||||
|
while read -r app; do
|
||||||
|
app_id=$(echo "$app" | awk -F'/' '{print $NF}')
|
||||||
|
alias_name=$(echo "$app_id" | awk -F'.' '{print $NF}' | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo "alias $alias_name='flatpak run $app_id'" >> "${flatpak_alias_file}"
|
||||||
|
done < <(find "$flatpak_binaries_dir" -maxdepth 1 -mindepth 1)
|
||||||
|
|
||||||
|
echo "Done generating Flatpak aliases"
|
||||||
|
echo "Check ${BLD}${BLU}$flatpak_alias_file${RST} to modify auto-generated alias"
|
||||||
|
else
|
||||||
|
echo "${RED}${BLD}Error: ${YLW}$flatpak_binaries_dir${RST} directory does not exist"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
export_flatpak_apps() {
|
||||||
|
local flatpak_export_file=""${1:-$FLATPAK_EXPORT_FILE}
|
||||||
|
flatpak list --columns=application --app > "${flatpak_export_file}"
|
||||||
|
echo "${BLD}${BLU}Flatpak apps exported successfully${RST}"
|
||||||
|
}
|
||||||
|
|
||||||
|
import_flatpak_apps() {
|
||||||
|
local flatpak_export_file=${1:-$FLATPAK_EXPORT_FILE}
|
||||||
|
xargs flatpak install -y < "${flatpak_export_file}"
|
||||||
|
}
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
echo -e "${BLD}Usage: ${GRN}fp${RST}${YLW} [OPTION]${RST}"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${BLD} Options:${RST}"
|
||||||
|
echo -e "${YLW} -e, export ${RST}Export a list of installed Flatpak apps to a file"
|
||||||
|
echo -e "${YLW} -i, import ${RST}Install Flatpak apps from exported file"
|
||||||
|
echo -e "${YLW} -g, generate ${RST}Generate aliases for all installed Flatpak apps"
|
||||||
|
echo -e "${YLW} -h, --help ${RST}Show this help"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local action=$1
|
||||||
|
|
||||||
|
case $action in
|
||||||
|
"generate"|"-g")
|
||||||
|
echo -en "Enter the location to save the Flatpak alias file, \n(default: ${FLATPAK_ALIAS_FILE}): "
|
||||||
|
read -r user_alias_dir
|
||||||
|
if [[ -n "$user_alias_dir" ]]; then
|
||||||
|
FLATPAK_ALIAS_FILE="$user_alias_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
generate_flatpak_alias "${FLATPAK_ALIAS_FILE}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
"export"|"-e")
|
||||||
|
echo -en "Enter the location to save the Flatpak export file (default: ${FLATPAK_EXPORT_FILE}): "
|
||||||
|
read -r user_export_file
|
||||||
|
if [[ -n "$user_export_file" ]]; then
|
||||||
|
FLATPAK_EXPORT_FILE="$user_export_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export_flatpak_apps "${FLATPAK_EXPORT_FILE}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
"import"|"-i")
|
||||||
|
echo -en "Enter the location of the Flatpak export file to import (default: ${FLATPAK_EXPORT_FILE}): "
|
||||||
|
read -r user_export_file
|
||||||
|
if [[ -n "$user_export_file" ]]; then
|
||||||
|
FLATPAK_EXPORT_FILE="$user_export_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
import_flatpak_apps "${FLATPAK_EXPORT_FILE}"
|
||||||
|
;;
|
||||||
|
"--help"|"-h")
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
main "$@"
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/zsh
|
#!/usr/bin/zsh
|
||||||
|
|
||||||
update_path() {
|
update_path() {
|
||||||
export PATH="$PATH:$1"
|
export PATH="$PATH:$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set manually
|
# Set manually
|
||||||
|
|
@ -27,7 +27,7 @@ update_path "/usr/local/bin"
|
||||||
update_path "$HOME/.local/bin"
|
update_path "$HOME/.local/bin"
|
||||||
update_path "$HOME/.cargo/bin/"
|
update_path "$HOME/.cargo/bin/"
|
||||||
update_path "$HOME/.spicetify"
|
update_path "$HOME/.spicetify"
|
||||||
update_path "$HOME/Repos/Private/scripts"
|
update_path "$DOTS/scripts"
|
||||||
update_path "$HOME/bin"
|
update_path "$HOME/bin"
|
||||||
update_path "$NPM_PACKAGES/bin"
|
update_path "$NPM_PACKAGES/bin"
|
||||||
update_path "$PNPM_HOME"
|
update_path "$PNPM_HOME"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Start mesuring bottlenecks
|
# Start mesuring bottlenecks
|
||||||
if [[ "$ZPROF" = true ]]; then
|
if [[ "$ZPROF" = true ]]; then
|
||||||
zmodload zsh/zprof
|
zmodload zsh/zprof
|
||||||
fi
|
fi
|
||||||
|
|
||||||
## Stuff I don't know what they're for
|
## Stuff I don't know what they're for
|
||||||
|
|
@ -53,33 +53,33 @@ zstyle ':omz:plugins:nvm' lazy true
|
||||||
ZSHZ_TILDE=1
|
ZSHZ_TILDE=1
|
||||||
|
|
||||||
plugins=(
|
plugins=(
|
||||||
nvm
|
nvm
|
||||||
alias-finder
|
alias-finder
|
||||||
archlinux
|
# archlinux
|
||||||
bgnotify
|
bgnotify
|
||||||
colored-man-pages
|
colored-man-pages
|
||||||
cp # alias to use rsync to copy files
|
cp # alias to use rsync to copy files
|
||||||
docker
|
docker
|
||||||
docker-compose
|
docker-compose
|
||||||
fd
|
fd
|
||||||
fzf
|
fzf
|
||||||
git
|
git
|
||||||
git-prompt
|
git-prompt
|
||||||
npm
|
npm
|
||||||
ripgrep
|
ripgrep
|
||||||
rsync
|
rsync
|
||||||
safe-paste # don't run code when pasting
|
safe-paste # don't run code when pasting
|
||||||
systemd
|
systemd
|
||||||
tmux
|
# tmux
|
||||||
# vi-mode
|
# vi-mode
|
||||||
yarn
|
yarn
|
||||||
z
|
z
|
||||||
zsh-autocomplete
|
zsh-autocomplete
|
||||||
zsh-autopair
|
zsh-autopair
|
||||||
zsh-autosuggestions
|
zsh-autosuggestions
|
||||||
zsh-completions
|
zsh-completions
|
||||||
zsh-syntax-highlighting
|
zsh-syntax-highlighting
|
||||||
zsh-interactive-cd
|
zsh-interactive-cd
|
||||||
)
|
)
|
||||||
|
|
||||||
# If not running interactively, don't do anything
|
# If not running interactively, don't do anything
|
||||||
|
|
@ -87,6 +87,11 @@ plugins=(
|
||||||
|
|
||||||
export HISTCONTROL=ignoreboth:erasedups
|
export HISTCONTROL=ignoreboth:erasedups
|
||||||
|
|
||||||
|
# autocomplete: https://github.com/marlonrichert/zsh-autocomplete/blob/main/.zshrc
|
||||||
|
zstyle ':autocomplete:*' fzf-completion yes
|
||||||
|
zstyle ':autocomplete:*' min-input 1
|
||||||
|
zstyle ':autocomplete:*' widget-style menu-select # Tab select instead of autocomplete
|
||||||
|
|
||||||
source $ZSH/oh-my-zsh.sh
|
source $ZSH/oh-my-zsh.sh
|
||||||
|
|
||||||
#--------------------------------------------------------------------#
|
#--------------------------------------------------------------------#
|
||||||
|
|
@ -95,20 +100,22 @@ source $ZSH/oh-my-zsh.sh
|
||||||
|
|
||||||
# File directory that are needed to source
|
# File directory that are needed to source
|
||||||
files=(
|
files=(
|
||||||
# zsh
|
# zsh
|
||||||
$DOTS/config/zsh/**/*.zsh
|
$DOTS/config/zsh/**/*.zsh
|
||||||
)
|
# fzf scripts that need to be sourced
|
||||||
|
$DOTS/scripts/fzf-flatpak
|
||||||
|
)
|
||||||
|
|
||||||
for file in $files; do
|
for file in $files; do
|
||||||
if [[ -f $file ]]; then
|
if [[ -f $file ]]; then
|
||||||
emulate -L zsh
|
emulate -L zsh
|
||||||
source $file
|
source $file
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
||||||
if [[ "$ZPROF" = true ]]; then
|
if [[ "$ZPROF" = true ]]; then
|
||||||
zprof
|
zprof
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
|
||||||
|
|
|
||||||
19
exports/flatpak-apps.txt
Normal file
19
exports/flatpak-apps.txt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
com.brave.Browser
|
||||||
|
com.github.iwalton3.jellyfin-media-player
|
||||||
|
com.github.qarmin.czkawka
|
||||||
|
com.github.tchx84.Flatseal
|
||||||
|
com.hunterwittenborn.Celeste
|
||||||
|
com.neatdecisions.Detwinner
|
||||||
|
com.slack.Slack
|
||||||
|
com.spotify.Client
|
||||||
|
in.srev.guiscrcpy
|
||||||
|
io.bassi.Amberol
|
||||||
|
io.beekeeperstudio.Studio
|
||||||
|
io.github.realmazharhussain.GdmSettings
|
||||||
|
io.gitlab.librewolf-community
|
||||||
|
me.kozec.syncthingtk
|
||||||
|
nz.mega.MEGAsync
|
||||||
|
org.gnome.World.PikaBackup
|
||||||
|
org.kde.kid3
|
||||||
|
org.signal.Signal
|
||||||
|
re.sonny.Junction
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
|
|
||||||
check-dependencies megatools rclone rsync fd
|
|
||||||
|
|
||||||
source "$DOTS/config/zsh/config/colors.zsh" && define_colors
|
source "$DOTS/config/zsh/config/colors.zsh" && define_colors
|
||||||
|
|
||||||
LOCAL_BACKUP_PATH="$HOME/Drives/Backups/auto-backups/"
|
LOCAL_BACKUP_PATH="$HOME/Drives/Backups/auto-backups/"
|
||||||
|
|
@ -17,6 +15,7 @@ DATE_FORMAT="+%F"
|
||||||
SOURCES=(
|
SOURCES=(
|
||||||
"$HOME/Drives/Stuff/Pictures/"
|
"$HOME/Drives/Stuff/Pictures/"
|
||||||
"$HOME/Drives/Stuff/Music/"
|
"$HOME/Drives/Stuff/Music/"
|
||||||
|
"root@berry.net:/home/aleidk/services/"
|
||||||
)
|
)
|
||||||
|
|
||||||
MAX_BACKUPS=10
|
MAX_BACKUPS=10
|
||||||
|
|
@ -56,7 +55,7 @@ backup() {
|
||||||
|
|
||||||
# shellcheck disable=2086
|
# shellcheck disable=2086
|
||||||
rsync \
|
rsync \
|
||||||
--super \
|
--fake-super \
|
||||||
--compress \
|
--compress \
|
||||||
--mkpath \
|
--mkpath \
|
||||||
--archive \
|
--archive \
|
||||||
|
|
@ -77,7 +76,7 @@ backup() {
|
||||||
# clean failed backups
|
# clean failed backups
|
||||||
for dir in $dst/.partial_*; do
|
for dir in $dst/.partial_*; do
|
||||||
date="$(echo "$dir" | cut -d "_" -f 2)"
|
date="$(echo "$dir" | cut -d "_" -f 2)"
|
||||||
rm -r "$dir" "${dst}/${date}"
|
rm -r "$dir" "${dst:?}/${date}"
|
||||||
done
|
done
|
||||||
echo -e ""
|
echo -e ""
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +105,7 @@ delete_old() {
|
||||||
}
|
}
|
||||||
|
|
||||||
sync() {
|
sync() {
|
||||||
config="$(rclone config dump 2>/dev/null | jq '."$RCLONE_CLOUD_NAME" // empty')"}
|
config="$(rclone config dump 2>/dev/null | jq ".$RCLONE_CLOUD_NAME // empty")"
|
||||||
|
|
||||||
if [[ -z "$config" ]]; then
|
if [[ -z "$config" ]]; then
|
||||||
echo -e "${RED}${SHL}No Rclone configuration! skiping sync.${RST}${EHL}\n"
|
echo -e "${RED}${SHL}No Rclone configuration! skiping sync.${RST}${EHL}\n"
|
||||||
|
|
@ -114,14 +113,14 @@ sync() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for dir in $LOCAL_BACKUP_PATH/*; do
|
for dir in $LOCAL_BACKUP_PATH/*; do
|
||||||
|
name="$(basename "$dir")"
|
||||||
remote_path="${RCLONE_CLOUD_NAME}:${REMOTE_BACKUP_PATH}/${name}"
|
remote_path="${RCLONE_CLOUD_NAME}:${REMOTE_BACKUP_PATH}/${name}"
|
||||||
newest="$(fd -t d --exact-depth 1 . "$dir" | sort -r | head -n 1)"
|
newest="$(fd -t d --exact-depth 1 . "$dir" | sort -r | head -n 1)"
|
||||||
name="$(basename "$dir")"
|
|
||||||
today=$(date "$DATE_FORMAT")
|
today=$(date "$DATE_FORMAT")
|
||||||
|
|
||||||
if [[ "$today" == "$(rclone cat "${remote_path}/.last-sync" 2>/dev/null)" ]]; then
|
if [[ "$today" == "$(rclone cat "${remote_path}/.last-sync" 2>/dev/null)" ]]; then
|
||||||
echo -e "${MGN}${DIM}Last sync for this backup was today, skiping...${RST}"
|
echo -e "${MGN}${DIM}Last sync ${BLU}for${RST} $name was today, skiping...${RST}"
|
||||||
return
|
continue
|
||||||
else
|
else
|
||||||
echo -e "Syncthing latest backup for ${BLU}$name${RST} in ${RED}$remote_path${RST}"
|
echo -e "Syncthing latest backup for ${BLU}$name${RST} in ${RED}$remote_path${RST}"
|
||||||
fi
|
fi
|
||||||
|
|
@ -143,7 +142,8 @@ ensure_path_exist "$LOCAL_BACKUP_PATH"
|
||||||
for path in "${SOURCES[@]}"; do
|
for path in "${SOURCES[@]}"; do
|
||||||
backup "$path"
|
backup "$path"
|
||||||
delete_old "$path"
|
delete_old "$path"
|
||||||
sync
|
|
||||||
done
|
done
|
||||||
|
|
||||||
|
sync
|
||||||
|
|
||||||
echo -e "${GRN}Backups done!${RST}"
|
echo -e "${GRN}Backups done!${RST}"
|
||||||
|
|
|
||||||
90
scripts/fzf-dnf
Executable file
90
scripts/fzf-dnf
Executable file
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
readonly basename="$(basename "$0")"
|
||||||
|
|
||||||
|
if ! hash fzf &> /dev/null; then
|
||||||
|
printf 'Error: Missing dep: fzf is required to use %s.\n' "${basename}" >&2
|
||||||
|
exit 64
|
||||||
|
fi
|
||||||
|
|
||||||
|
#Colors
|
||||||
|
declare -r esc=$'\033'
|
||||||
|
declare -r BLUE="${esc}[1m${esc}[34m"
|
||||||
|
declare -r RED="${esc}[31m"
|
||||||
|
declare -r GREEN="${esc}[32m"
|
||||||
|
declare -r YELLOW="${esc}[33m"
|
||||||
|
declare -r CYAN="${esc}[36m"
|
||||||
|
# Base commands
|
||||||
|
readonly QRY="dnf --cacheonly --quiet repoquery "
|
||||||
|
readonly PRVW="dnf --cacheonly --quiet --color=always info"
|
||||||
|
readonly QRY_PRFX=' '
|
||||||
|
readonly QRY_SFFX=' > '
|
||||||
|
# Install mode
|
||||||
|
readonly INS_QRYS="${QRY} --qf '${CYAN}%{name}'"
|
||||||
|
readonly INS_PRVW="${PRVW}"
|
||||||
|
readonly INS_PRMPT="${CYAN}${QRY_PRFX}Install packages${QRY_SFFX}"
|
||||||
|
# Remove mode
|
||||||
|
readonly RMV_QRYS="${QRY} --installed --qf '${RED}%{name}'"
|
||||||
|
readonly RMV_PRVW="${PRVW} --installed"
|
||||||
|
readonly RMV_PRMPT="${RED}${QRY_PRFX}Remove packages${QRY_SFFX}"
|
||||||
|
# Remove-userinstalled mode
|
||||||
|
readonly RUI_QRYS="${QRY} --userinstalled --qf '${YELLOW}%{name}'"
|
||||||
|
readonly RUI_PRVW="${PRVW} --installed"
|
||||||
|
readonly RUI_PRMPT="${YELLOW}${QRY_PRFX}Remove User-Installed${QRY_SFFX}"
|
||||||
|
# Updates mode
|
||||||
|
readonly UPD_QRY="${QRY} --upgrades --qf '${GREEN}%{name}'"
|
||||||
|
readonly UPD_QRYS="if [[ $(${UPD_QRY} | wc -c) -ne 0 ]]; then ${UPD_QRY}; else echo ${GREEN}No updates available.; echo Try refreshing metadata cache...; fi"
|
||||||
|
readonly UPD_PRVW="${PRVW}"
|
||||||
|
readonly UPD_PRMPT="${GREEN}${QRY_PRFX}Upgrade packages${QRY_SFFX}"
|
||||||
|
|
||||||
|
mapfile -d '' fhelp <<-EOF
|
||||||
|
|
||||||
|
"${basename}"
|
||||||
|
Interactive package manager for Fedora
|
||||||
|
|
||||||
|
Alt-i Install mode (default)
|
||||||
|
Alt-r Remove mode
|
||||||
|
Alt-e Remove User-Installed mode
|
||||||
|
Alt-u Updates mode
|
||||||
|
Alt-m Update package metadata cache
|
||||||
|
|
||||||
|
Enter Confirm selection
|
||||||
|
Tab Mark package ()
|
||||||
|
Shift-Tab Unmark package
|
||||||
|
Ctrl-a Select all
|
||||||
|
|
||||||
|
? Help (this page)
|
||||||
|
ESC Quit
|
||||||
|
EOF
|
||||||
|
|
||||||
|
declare tmp_file
|
||||||
|
if tmp_file="$(mktemp --tmpdir "${basename}".XXXXXX)"; then
|
||||||
|
printf 'in' > "${tmp_file}" &&
|
||||||
|
SHELL='/bin/bash' \
|
||||||
|
FZF_DEFAULT_COMMAND="${INS_QRYS}" \
|
||||||
|
fzf \
|
||||||
|
--ansi \
|
||||||
|
--multi \
|
||||||
|
--query=$* \
|
||||||
|
--header=" ${basename} | Press Alt+? for help or ESC to quit" \
|
||||||
|
--header-first \
|
||||||
|
--prompt="${INS_PRMPT}" \
|
||||||
|
--marker=' ' \
|
||||||
|
--preview-window='right,67%,wrap' \
|
||||||
|
--preview="${INS_PRVW} {1}" \
|
||||||
|
--bind="enter:execute(if grep -q 'in' \"${tmp_file}\"; then sudo dnf install {+};
|
||||||
|
elif grep -q 'rm' \"${tmp_file}\"; then sudo dnf remove {+}; \
|
||||||
|
elif grep -q 'up' \"${tmp_file}\"; then sudo dnf upgrade {+}; fi; \
|
||||||
|
read -s -r -n1 -p $'\n${BLUE}Press any key to continue...' && printf '\n')" \
|
||||||
|
--bind="alt-i:unbind(alt-i)+reload(${INS_QRYS})+change-preview(${INS_PRVW} {1})+change-prompt(${INS_PRMPT})+execute-silent(printf 'in' > \"${tmp_file}\")+first+rebind(alt-r,alt-e,alt-u)" \
|
||||||
|
--bind="alt-r:unbind(alt-r)+reload(${RMV_QRYS})+change-preview(${RMV_PRVW} {1})+change-prompt(${RMV_PRMPT})+execute-silent(printf 'rm' > \"${tmp_file}\")+first+rebind(alt-i,alt-e,alt-u)" \
|
||||||
|
--bind="alt-e:unbind(alt-e)+reload(${RUI_QRYS})+change-preview(${RUI_PRVW} {1})+change-prompt(${RUI_PRMPT})+execute-silent(printf 'rm' > \"${tmp_file}\")+first+rebind(alt-i,alt-r,alt-u)" \
|
||||||
|
--bind="alt-u:unbind(alt-u)+reload(${UPD_QRYS})+change-preview(${UPD_PRVW} {1})+change-prompt(${UPD_PRMPT})+execute-silent(printf 'up' > \"${tmp_file}\")+first+rebind(alt-i,alt-r,alt-e)" \
|
||||||
|
--bind="alt-m:execute(sudo dnf makecache;read -s -r -n1 -p $'\n${BLUE}Press any key to continue...' && printf '\n')" \
|
||||||
|
--bind="alt-?:preview(printf \"${fhelp[0]}\")" \
|
||||||
|
--bind="ctrl-a:select-all"
|
||||||
|
|
||||||
|
rm -f "${tmp_file}" &> /dev/null
|
||||||
|
else
|
||||||
|
printf 'Error: Failed to create tmp file. $TMPDIR (or /tmp if $TMPDIR is unset) may not be writable.\n' >&2
|
||||||
|
exit 65
|
||||||
|
fi
|
||||||
175
scripts/fzf-flatpak
Executable file
175
scripts/fzf-flatpak
Executable file
|
|
@ -0,0 +1,175 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
# CLR=$(for i in {0..7}; do echo "tput setaf $i"; done)
|
||||||
|
BLK=\$(tput setaf 0); RED=\$(tput setaf 1); GRN=\$(tput setaf 2); YLW=\$(tput setaf 3); BLU=\$(tput setaf 4);
|
||||||
|
MGN=\$(tput setaf 5); CYN=\$(tput setaf 6); WHT=\$(tput setaf 7); BLD=\$(tput bold); RST=\$(tput sgr0);
|
||||||
|
|
||||||
|
AWK_COLOR_VAR=" -v BLK=${BLK} -v RED=${RED} -v GRN=${GRN} -v YLW=${YLW} -v BLU=${BLU} -v MGN=${MGN} -v CYN=${CYN} -v WHT=${WHT} -v BLD=${BLD} -v RST=${RST}"
|
||||||
|
|
||||||
|
FZF_FLATPAK_HELP="$(
|
||||||
|
cat <<-EOF
|
||||||
|
|
||||||
|
${BLU}${BLD}Fzf-Flatpak.sh
|
||||||
|
|
||||||
|
${BLU}${BLD}M-f M-i ${RST}${CYN}Install apps (flathub repo)
|
||||||
|
${BLU}${BLD}M-f M-u ${RST}${CYN}Uninstall apps
|
||||||
|
${BLU}${BLD}M-f M-r ${RST}${CYN}Run apps
|
||||||
|
|
||||||
|
${BLU}${BLD}M-? ${RST}${CYN}Help (this page)
|
||||||
|
${BLU}${BLD}ESC ${RST}${CYN}Exit
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ $- =~ i ]]; then
|
||||||
|
#-----------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
_fzf_flatpak_fzf() {
|
||||||
|
fzf-tmux -p85% -- \
|
||||||
|
--tiebreak=begin \
|
||||||
|
-m --ansi --nth=1.. \
|
||||||
|
--color='header:italic:underline' \
|
||||||
|
--color='fg+:blue,border:blue' \
|
||||||
|
--layout=reverse --height=50% --border \
|
||||||
|
--preview-window "nohidden,50%,<50(down,60%,border-rounded)" \
|
||||||
|
--bind "del:execute(flatpak remove --unused > /dev/tty; read -r)" \
|
||||||
|
--bind="alt-?:preview(printf \"${FZF_FLATPAK_HELP}\")" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
_fzf_flatpak_check() {
|
||||||
|
which flatpak > /dev/null 2>&1 && return
|
||||||
|
|
||||||
|
[[ -n $TMUX ]] && tmux display-message "Flatpak Isn't Installed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
__fzf_flatpak=${BASH_SOURCE[0]:-${(%):-%x}}
|
||||||
|
__fzf_flatpak=$(readlink -f "$__fzf_flatpak" 2> /dev/null || /usr/bin/ruby --disable-gems -e 'puts File.expand_path(ARGV.first)' "$__fzf_flatpak" 2> /dev/null)
|
||||||
|
|
||||||
|
_fzf_flatpak_install() {
|
||||||
|
_fzf_flatpak_check || return
|
||||||
|
flatpak remote-ls flathub --cached --columns=app,name,description 2>/dev/null \
|
||||||
|
| awk -v cyn=$(tput setaf 6) -v blu=$(tput setaf 4) -v bld=$(tput bold) -v res=$(tput sgr0) \
|
||||||
|
'{
|
||||||
|
app_info="";
|
||||||
|
for(i=3;i<=NF;i++){
|
||||||
|
app_info=app_info" "$i
|
||||||
|
}; print blu bld $2" -" res cyn app_info "|" $1}' \
|
||||||
|
| column -t -s "|" -R 3 \
|
||||||
|
| _fzf_flatpak_fzf \
|
||||||
|
--prompt=" Install > " \
|
||||||
|
--header=$'M-u: Update / Del: Remove Unused \n\n' \
|
||||||
|
--preview "flatpak --system remote-info flathub {-1} | awk $AWK_COLOR_VAR -F\":\" '{print YLW BLD \$1 RST MGN \$2}'" \
|
||||||
|
--bind="alt-u:execute(flatpak update > /dev/tty; read -r)" \
|
||||||
|
--bind="alt-m:change-preview(flatpak metadata {-1})" \
|
||||||
|
--bind "enter:execute(flatpak install flathub {+-1} > /dev/tty)+clear-screen" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# _fzf_flatpak_remotes?() {
|
||||||
|
# flatpak remotes --columns=name | tail -n +1
|
||||||
|
# }
|
||||||
|
|
||||||
|
# _fzf_flatpak_format_installed_lists() {
|
||||||
|
# local color1 color2 bold reset
|
||||||
|
# color1="$1"
|
||||||
|
# color2="$2"
|
||||||
|
# bold="$(tput bold)"
|
||||||
|
# reset="$(tput sgr0)"
|
||||||
|
# awk -v c1="$color1" -v c2="$color2" -v bld="$bold" -v res="$reset" \
|
||||||
|
# '{
|
||||||
|
# app_id="";
|
||||||
|
# for(i=2;i<=NF;i++){
|
||||||
|
# app_id=app_id" "$i
|
||||||
|
# }; print bld c1 app_id " && - " res c2 $1}' \
|
||||||
|
# | column -t -s "&&"
|
||||||
|
# }
|
||||||
|
|
||||||
|
_fzf_flatpak_installed_lists() {
|
||||||
|
awk -v cyn=$(tput setaf 6) -v blu=$(tput setaf 4) -v bld=$(tput bold) -v res=$(tput sgr0) \
|
||||||
|
'{
|
||||||
|
app_id="";
|
||||||
|
for(i=2;i<=NF;i++){
|
||||||
|
app_id=app_id" "$i
|
||||||
|
}; print bld cyn app_id " && - " res blu $1}' \
|
||||||
|
| column -t -s "&&"
|
||||||
|
}
|
||||||
|
|
||||||
|
_fzf_flatpak_uninstall_lists() {
|
||||||
|
awk -v mgn=$(tput setaf 5) -v red=$(tput setaf 1) -v bld=$(tput bold) -v res=$(tput sgr0) \
|
||||||
|
'{
|
||||||
|
app_id="";
|
||||||
|
for(i=2;i<=NF;i++){
|
||||||
|
app_id=app_id" "$i
|
||||||
|
}; print bld mgn app_id " && - " res red $1}' \
|
||||||
|
| column -t -s "&&"
|
||||||
|
}
|
||||||
|
|
||||||
|
_fzf_flatpak_installed_lists-applications() {
|
||||||
|
flatpak list --app --columns=application,name | _fzf_flatpak_installed_lists
|
||||||
|
}
|
||||||
|
|
||||||
|
_fzf_flatpak_uninstall_lists_with_runtimes() {
|
||||||
|
flatpak list --columns=application,name | _fzf_flatpak_uninstall_lists
|
||||||
|
}
|
||||||
|
|
||||||
|
# `flatpak run` only accepts one argument so it isn't possible to run multiple apps
|
||||||
|
_fzf_flatpak_fzf_installed_lists() {
|
||||||
|
_fzf_flatpak_fzf \
|
||||||
|
--header=$'M-u: Uninstall / Del: Remove Unused / F4: Kill / M-r: Run\n\n' \
|
||||||
|
--bind "f4:execute(flatpak kill {+-1})" \
|
||||||
|
--bind "alt-r:change-prompt( Run > )+execute-silent(touch /tmp/run && rm -r /tmp/uns)" \
|
||||||
|
--bind "alt-u:change-prompt( Uninstall > )+execute-silent(touch /tmp/uns && rm -r /tmp/run)" \
|
||||||
|
--bind "enter:execute(
|
||||||
|
if [ -f /tmp/uns ]; then
|
||||||
|
flatpak uninstall {+-1} > /dev/tty;
|
||||||
|
elif [ -f /tmp/run ]; then
|
||||||
|
flatpak run {-1} > /dev/null;
|
||||||
|
fi
|
||||||
|
)" "$@"
|
||||||
|
rm -f /tmp/{uns,run} &> /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
_fzf_flatpak_uninstall() {
|
||||||
|
_fzf_flatpak_check || return
|
||||||
|
touch /tmp/uns
|
||||||
|
_fzf_flatpak_uninstall_lists_with_runtimes | _fzf_flatpak_fzf_installed_lists \
|
||||||
|
--prompt=" Uninstall > " \
|
||||||
|
--preview "flatpak info {-1} | awk $AWK_COLOR_VAR -F\":\" '{print RED BLD \$1 RST MGN \$2}'" \
|
||||||
|
}
|
||||||
|
|
||||||
|
_fzf_flatpak_run_apps() {
|
||||||
|
_fzf_flatpak_check || return
|
||||||
|
touch /tmp/run
|
||||||
|
_fzf_flatpak_installed_lists-applications | _fzf_flatpak_fzf_installed_lists \
|
||||||
|
--prompt=" Run > " \
|
||||||
|
--preview "flatpak info {-1} | awk $AWK_COLOR_VAR -F\":\" '{print CYN BLD \$1 RST BLU \$2}'" \
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -n "${BASH_VERSION:-}" ]]; then
|
||||||
|
__fzf_flatpak_init() {
|
||||||
|
bind '"\er": redraw-current-line'
|
||||||
|
local o
|
||||||
|
for o in "$@"; do
|
||||||
|
bind '"\M-f\M-'${o:0:1}'": "`_fzf_flatpak_'$o'`\e\M-e\er"'
|
||||||
|
bind '"\M-f'${o:0:1}'": "`_fzf_flatpak_'$o'`\e\M-e\er"'
|
||||||
|
done
|
||||||
|
}
|
||||||
|
elif [[ -n "${ZSH_VERSION:-}" ]]; then
|
||||||
|
__fzf_flatpak_join() {
|
||||||
|
local item
|
||||||
|
while read item; do
|
||||||
|
echo -n "${(q)item} "
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
__fzf_flatpak_init() {
|
||||||
|
local o
|
||||||
|
zle
|
||||||
|
for o in "$@"; do
|
||||||
|
eval "fzf-flatpak-$o-widget() { local result=\$(_fzf_flatpak_$o | __fzf_flatpak_join); zle && { zle reset-prompt; zle -R }; LBUFFER+=\$result }"
|
||||||
|
eval "zle -N fzf-flatpak-$o-widget"
|
||||||
|
eval "bindkey '^[f^[${o[1]}' fzf-flatpak-$o-widget"
|
||||||
|
eval "bindkey '^[f[${o[1]}' fzf-flatpak-$o-widget"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
__fzf_flatpak_init install uninstall run_apps
|
||||||
|
#-----------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
fi
|
||||||
|
|
@ -8,8 +8,40 @@ WALLPAPER_PREFIX="Fondos_PC.*"
|
||||||
RANDOM_PICTURE=$(fd -apt f "$WALLPAPER_PREFIX" "$WALLPAPER_DIR" | shuf -n 1)
|
RANDOM_PICTURE=$(fd -apt f "$WALLPAPER_PREFIX" "$WALLPAPER_DIR" | shuf -n 1)
|
||||||
CURRENT_DESKTOP="$(echo "$XDG_CURRENT_DESKTOP" | awk '{for (i=1;i<=NF;i++) { $i=toupper(substr($i,1,1)) tolower(substr($i,2)) }}1')"
|
CURRENT_DESKTOP="$(echo "$XDG_CURRENT_DESKTOP" | awk '{for (i=1;i<=NF;i++) { $i=toupper(substr($i,1,1)) tolower(substr($i,2)) }}1')"
|
||||||
|
|
||||||
|
# Extra actions
|
||||||
|
declare -A CONFIG
|
||||||
|
CONFIG["delete_current"]=false
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
echo -e "${BLD}Usage: ${GRN}set-random-wallpaper${RST}${YLW} [OPTION]${RST}"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${BLD} Options:${RST}"
|
||||||
|
echo -e "${YLW} -d, delete ${RST}delete current wallpaper"
|
||||||
|
echo -e "${YLW} -h, --help ${RST}Show this help"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
delete | -d)
|
||||||
|
CONFIG["delete_current"]=true
|
||||||
|
;;
|
||||||
|
help | -h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
shift
|
||||||
|
|
||||||
case "$CURRENT_DESKTOP" in
|
case "$CURRENT_DESKTOP" in
|
||||||
Gnome)
|
Gnome)
|
||||||
|
|
||||||
|
if [[ "${CONFIG[delete_current]}" == true ]]; then
|
||||||
|
file_path="$(gsettings get org.gnome.desktop.background picture-uri)"
|
||||||
|
file_path="${file_path:8:-1}"
|
||||||
|
|
||||||
|
rm -iv "$file_path"
|
||||||
|
fi
|
||||||
|
|
||||||
gsettings set org.gnome.desktop.background picture-uri "file://$RANDOM_PICTURE"
|
gsettings set org.gnome.desktop.background picture-uri "file://$RANDOM_PICTURE"
|
||||||
gsettings set org.gnome.desktop.background picture-uri-dark "file://$RANDOM_PICTURE"
|
gsettings set org.gnome.desktop.background picture-uri-dark "file://$RANDOM_PICTURE"
|
||||||
;;
|
;;
|
||||||
|
|
@ -32,7 +64,7 @@ KDE)
|
||||||
d.currentConfigGroup = Array("Wallpaper","org.kde.image","General");
|
d.currentConfigGroup = Array("Wallpaper","org.kde.image","General");
|
||||||
d.writeConfig("Image", "file:///'$RANDOM_PICTURE'")
|
d.writeConfig("Image", "file:///'$RANDOM_PICTURE'")
|
||||||
}}
|
}}
|
||||||
'
|
'
|
||||||
;;
|
;;
|
||||||
Sway)
|
Sway)
|
||||||
MODES=(
|
MODES=(
|
||||||
|
|
|
||||||
9
setup.sh
9
setup.sh
|
|
@ -44,6 +44,15 @@ backup() {
|
||||||
|
|
||||||
symlink() {
|
symlink() {
|
||||||
local dootsfile="$1" target_dir="$2"
|
local dootsfile="$1" target_dir="$2"
|
||||||
|
|
||||||
|
final_path="$target_dir/$(basename "$dootsfile")"
|
||||||
|
backup_file=$final_path.dots
|
||||||
|
|
||||||
|
if [[ -e "$final_path" ]] && [[ ! -L "$final_path" ]] && [[ ! -e "$backup_file" ]]; then
|
||||||
|
echo "Backing up ${BLD}${BLU}$target_dir${RST} to ${BLD}${CYN}$backup_file${RST}..."
|
||||||
|
mv "$final_path" "$backup_file" && sleep 0.3
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "Symlinking ${BLD}${BLU}$dootsfile${RST} to ${BLD}${GRN}$target_dir${RST}..."
|
echo -e "Symlinking ${BLD}${BLU}$dootsfile${RST} to ${BLD}${GRN}$target_dir${RST}..."
|
||||||
ln -sf "$dootsfile" "$target_dir" && sleep 0.3
|
ln -sf "$dootsfile" "$target_dir" && sleep 0.3
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue