Compare commits

..

34 Commits

Author SHA1 Message Date
6eadea6f00 update to fork 2025-09-08 14:36:43 -07:00
05adde2559 update link 2025-09-08 14:06:22 -07:00
7579628faf update input.conf 2025-09-08 14:04:55 -07:00
5581723522 revert back to fork 2025-09-08 14:04:39 -07:00
b6390baec6 update submodule 2025-09-07 15:40:24 -07:00
21998241ba update mpvacious submodule and add keybinds to input.conf 2025-09-03 20:37:53 -07:00
57f6a5db30 update 2025-09-03 20:20:32 -07:00
5a6ac4bb3a Remove mpvacious submodule 2025-09-03 20:14:37 -07:00
5f08f09b2c add ytdl-preload script-opts file 2025-08-19 00:24:59 -07:00
9819a64f01 add immersion tracker 2025-08-19 00:16:22 -07:00
73a305af0b update mpv config 2025-08-17 17:31:36 -07:00
9ac84aba18 update submodule paths 2025-08-17 17:31:31 -07:00
cfc6ac22e5 Stage new submodule locations 2025-08-17 16:59:41 -07:00
e4afe79832 Move submodules to submodules directory 2025-08-17 16:57:56 -07:00
ca0899d233 enable watch history 2025-05-27 00:30:17 -07:00
deb591481e update configs 2025-05-27 00:30:00 -07:00
472899b680 update mac conf 2025-05-26 04:32:13 -07:00
2f3ccc60fc update mac conf 2025-05-26 04:19:01 -07:00
02d3c00229 update anilist to submodule 2025-05-26 02:19:45 -07:00
f3a4afcf76 update 2025-05-26 01:32:12 -07:00
087f16dfd0 update subs2srs 2025-05-24 21:23:18 -07:00
0b9f814eca add back mpvacious 2025-05-02 01:07:28 -07:00
efb9737e14 remove mpvacious 2025-05-01 19:38:05 -07:00
56e477bff1 update modernz 2025-04-28 23:48:50 -07:00
04fabf6356 add other shaders 2025-04-12 01:18:10 -07:00
d9747459d8 update config 2025-04-10 17:44:19 -07:00
33ea15228e update 2025-04-08 23:46:52 -07:00
1cd68861f5 add prettier file 2025-04-06 23:02:51 -07:00
2c90b30993 add jimaku script and update subsync 2025-04-06 23:02:06 -07:00
382c29ba42 update to alass 2025-04-06 22:25:05 -07:00
37b67be69b update 2025-04-05 19:47:37 -07:00
2ed3918aca update config 2025-04-05 19:46:45 -07:00
f0237be035 update script to use virtual environment 2025-04-04 03:43:22 -07:00
5260564aaf add mpvacious 2025-04-04 00:23:45 -07:00
43 changed files with 12006 additions and 2354 deletions

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@ immersive-data
.ytdl_preload/ .ytdl_preload/
.ytdl-preload/ .ytdl-preload/
scripts/anilistUpdater/anilistToken.txt scripts/anilistUpdater/anilistToken.txt
.watch-later
state

33
.gitmodules vendored
View File

@@ -1,18 +1,31 @@
[submodule "mpv-youtube-upnext"] [submodule "mpv-youtube-upnext"]
path = mpv-youtube-upnext path = submodules/mpv-youtube-upnext
url = https://github.com/ksyasuda/mpv-youtube-upnext url = https://github.com/ksyasuda/mpv-youtube-upnext
[submodule "mpv-youtube-queue"] [submodule "mpv-youtube-queue"]
path = mpv-youtube-queue path = submodules/mpv-youtube-queue
url = https://github.com/ksyasuda/mpv-youtube-queue url = https://github.com/ksyasuda/mpv-youtube-queue
[submodule "ModernZ"] [submodule "ModernZ"]
path = ModernZ path = submodules/ModernZ
url = git@github.com:Samillion/ModernZ.git url = git@github.com:Samillion/ModernZ.git
[submodule "ytdl-preload"] [submodule "ytdl-preload"]
path = ytdl-preload path = submodules/ytdl-preload
url = git@gist.github.com:17d90e3deeb35b5f75e55adb19098f58.git url = git@gist.github.com:17d90e3deeb35b5f75e55adb19098f58.git
[submodule "autosubsync-mpv"] [submodule "mpv-anilist-updater"]
path = autosubsync-mpv path = submodules/mpv-anilist-updater
url = git@github.com:Ajatt-Tools/autosubsync-mpv.git url = git@github.com:AzuredBlue/mpv-anilist-updater.git
[submodule "scripts/autosubsync-mpv"] [submodule "thumbfast"]
path = scripts/autosubsync-mpv path = submodules/thumbfast
url = git@github.com:Ajatt-Tools/autosubsync-mpv.git url = git@github.com:po5/thumbfast.git
[submodule "submodules/autosubsync-mpv"]
path = submodules/autosubsync-mpv
url = git@github.com:joaquintorres/autosubsync-mpv.git
[submodule "submodules/immersion-tracker"]
path = submodules/immersion-tracker
url = git@gitea.suda.codes:sudacode/immersion-tracker.git
[submodule "submodules/mpvacious"]
path = submodules/mpvacious
url = git@github.com:ksyasuda/mpvacious.git
[submodule "submodules/animecards"]
path = submodules/animecards
url = git@github.com:ksyasuda/Anacreon-Script

2
.prettierrc Normal file
View File

@@ -0,0 +1,2 @@
trailingComma: "es5"
singleQuote: true

Submodule ModernZ deleted from 2d5537aa72

View File

@@ -193,19 +193,89 @@ CTRL+1 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/FSR.glsl"; sho
CTRL+2 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/NVScaler.glsl"; show-text "NIS" CTRL+2 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/NVScaler.glsl"; show-text "NIS"
CTRL+3 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/CAS-scaled.glsl"; show-text "CAS" CTRL+3 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/CAS-scaled.glsl"; show-text "CAS"
CTRL+4 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/FSRCNNX.glsl"; show-text "FSRCNNX" CTRL+4 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/FSRCNNX.glsl"; show-text "FSRCNNX"
CTRL+5 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/FSRCNNX.glsl:~/.config/mpv/shaders/CAS-scaled.glsl:~/.config/mpv/shaders/NVScaler.glsl"; show-text "ALL" CTRL+5 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/ArtCNN_C4F32.glsl"; show-text "ArtCNN"
CTRL+6 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/ArtCNN_C4F16_DS.glsl"; show-text "ArtCNN"
CTRL+7 no-osd change-list glsl-shaders set "~/.config/mpv/shaders/ArtCNN_C4F16.glsl"; show-text "ArtCNN"
CTRL+0 no-osd change-list glsl-shaders clr ""; show-text "GLSL shaders cleared" CTRL+0 no-osd change-list glsl-shaders clr ""; show-text "GLSL shaders cleared"
ctrl+K cycle-values keep-open "yes" "no" ctrl+K cycle-values keep-open "yes" "no"
ctrl+r script-binding reload-scripts ctrl+r script-binding reload-scripts
ctrl+s script_binding autosubsync-menu ctrl+s script_binding autosubsync-menu
# {{{ sponsorblock
ctrl+g script-binding sponsorblock/set_segment ctrl+g script-binding sponsorblock/set_segment
ctrl+G script-binding sponsorblock/submit_segment ctrl+G script-binding sponsorblock/submit_segment
ctrl+h script-binding sponsorablock/upvote_setment ctrl+< script-binding sponsorblock/upvote_setment
ctrl+H script-binding sponsorablock/downvote_segment ctrl+> script-binding sponsorblock/downvote_segment
#}}}
# {{{ anilist
ctrl+A script-binding update_anilist ctrl+A script-binding update_anilist
ctrl+B script-binding launch_anilist ctrl+B script-binding launch_anilist
ctrl+E script-binding open_folder ctrl+E script-binding open_folder
ctrl+V script-binding mpvacious-secondary-sid-toggle
# }}}
# {{{ mpvacious (subs2srs)
a script-binding mpvacious-menu-open
ctrl+g ignore # script-binding mpvacious-animated-snapshot-toggle
ctrl+N script-binding mpvacious-export-note
ctrl+b ignore # script-binding mpvacious-update-selected-note
ctrl+B ignore # script-binding mpvacious-overwrite-selected-note
ctrl+m ignore # script-binding mpvacious-update-last-note
ctrl+M ignore # script-binding mpvacious-overwrite-last-note
G script-binding mpvacious-quick-card-menu-open
alt+g script-binding mpvacious-quick-card-sel-menu-open
ctrl+c script-binding mpvacious-copy-primary-sub-to-clipboard
ctrl+C script-binding mpvacious-copy-secondary-sub-to-clipboard
ctrl+= script-binding mpvacious-autocopy-toggle
H script-binding mpvacious-sub-seek-back
L script-binding mpvacious-sub-seek-forward
alt+h script-binding mpvacious-sub-seek-back-pause
alt+l script-binding mpvacious-sub-seek-forward-pause
ctrl+h script-binding mpvacious-sub-rewind
ctrl+H script-binding mpvacious-sub-replay
ctrl+L script-binding mpvacious-sub-play-up-to-next
ctrl+V script-binding mpvacious-secondary-sid-toggle
ctrl+K script-binding mpvacious-secondary-sid-prev
ctrl+J script-binding mpvacious-secondary-sid-next
# }}}
# {{ animecards
ctrl+v script-binding animecards/update-anki-card
# }}
# {{{ mpv-youtube-queue
ctrl+a script-binding mpv_youtube_queue/add_to_queue
ctrl+n script-binding mpv_youtube_queue/play_next_in_queue
ctrl+p script-binding mpv_youtube_queue/play_previous_in_queue
ctrl+q script-binding mpv_youtube_queue/print_queue
ctrl+k script-binding mpv_youtube_queue/move_cursor_up
ctrl+j script-binding mpv_youtube_queue/move_cursor_down
ctrl+ENTER script-binding mpv_youtube_queue/play_selected_video
ctrl+o script-binding mpv_youtube_queue/open_video_in_browser
ctrl+P script-binding mpv_youtube_queue/print_current_video
ctrl+O script-binding mpv_youtube_queue/open_channel_in_browser
ctrl+d script-binding mpv_youtube_queue/download_current_video
ctrl+D script-binding mpv_youtube_queue/download_selected_video
ctrl+m script-binding mpv_youtube_queue/move_video
ctrl+x script-binding mpv_youtube_queue/delete_video
#}}}
#{{{ MPV SELECT
g ignore
g-a script-binding select/select-aid
g-b script-binding select/select-binding
g-c script-binding select/select-chapter
g-e script-binding select/select-edition
g-h script-binding select/select-watch-history
g-l script-binding select/select-subtitle-line
g-m script-binding select/menu
g-p script-binding select/select-playlist
g-r script-binding select/show-properties
g-s script-binding select/select-sid
g-S script-binding select/select-secondary-sid
g-t script-binding select/select-track
g-v script-binding select/select-vid
g-w script-binding select/select-watch-later
# }}}

243
mac.conf Normal file
View File

@@ -0,0 +1,243 @@
#
# 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 ~~/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.
# window=scale=1.0
# Specify high quality video rendering preset (for --vo=gpu only)
# Can cause performance problems with some drivers and GPUs.
# profile=gpu-hq
# 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
# 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.
# discourged by mpv devs and not likely to make significant difference
# hwdec=auto-copy
# hwdec-codecs=all
##################
# 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 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
# user-agent="Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36"
# user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/37.0.2062.94 Chrome/37.0.2062.94 Safari/537.36"
# user-agent="Chromium/37.0.2062.94 Chrome/37.0.2062.94 Safari/537.36"
# 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
sub-font="JetBrainsMono Nerd Font"
sub-font-size=45
# osd-font="Fluent System Icons"
border=no
geometry=50%
volume=50
# speed-step=0.05
# audio-spdif=ac3,eac3,dts-hd,truehd
# glsl-shaders="~~/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"
glsl-shaders="~~/shaders/ArtCNN_C4F16.glsl"
# glsl-shaders="~~/shaders/FSR.glsl"
# Can fix stuttering in some cases, in other cases probably causes it. Try it if you experience stuttering.
opengl-early-flush=no
video-sync=display-resample
osc=no
no-border
ytdl-raw-options=sub-langs=en.*,write-auto-subs=
ytdl-format=bestvideo+bestaudio/best
# get subtitles for videos automatically
sub-auto=fuzzy
slang=en,eng
# CATPPUCCIN MACHIATTO
# Main mpv options
background-color='#24273a'
osd-back-color='#181926'
osd-border-color='#181926'
osd-color='#cad3f5'
osd-shadow-color='#24273a'
# Stats script options
# Options are on separate lines for clarity
# Colors are in #BBGGRR format
script-opts-append=stats-border_color=30201e
script-opts-append=stats-font_color=f5d3ca
script-opts-append=stats-plot_bg_border_color=f8bdb7
script-opts-append=stats-plot_bg_color=30201e
script-opts-append=stats-plot_color=f8bdb7
# profile=svp
profile=gpu-hq
# GPU OPTIONS
vo=gpu-next
# hwdec=nvdec-copy
hwdec=videotoolbox
scale=bicubic
dscale=bicubic
cscale=bicubic
tscale=oversample
interpolation=yes
interpolation-preserve=no
input-ipc-server=/tmp/mpvsocket
# ao=pule,pipewire
# ao=pipewire,pulse
ontop=yes
ao=coreaudio
save-position-on-quit
watch-later-dir="~~.watch-later"
resume-playback=yes
save-watch-history
watch-history-path="~~state/watch_history.jsonl"
vd-lavc-threads=0
gpu-api=vulkan
gpu-context=macvk
opengl-pbo=yes
[svp]
input-ipc-server=/tmp/mpvsocket # Receives input from SVP
hr-seek-framedrop=no # Fixes audio desync
resume-playback=no # Not compatible with SVP
[Idle]
profile-cond=p["idle-active"]
profile-restore=copy-equal
title=' '
keepaspect=no
[immersion]
cookies=yes
cookies-file=/Volumes/sudacode/japanese/cookies.Japanese.txt
ytdl-raw-options=mark-watched=,write-auto-subs=,sub-langs=ja.*
ytdl-raw-options-append=cookies=/Volumes/sudacode/japanese/cookies.Japanese.txt
ytdl-raw-options-append=sponsorblock-mark=all
ytdl-raw-options-append=sponsorblock-remove=sponsor
ytdl-format=bestvideo+bestaudio/best
# get subtitles for videos automatically
sub-auto=fuzzy
slang=ja,jpn
alang=ja,jpn
vlang=ja,jpn

242
mpv.conf
View File

@@ -1,162 +1,21 @@
#
# 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 ~~/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
# window=scale=1.0
# Specify high quality video rendering preset (for --vo=gpu only)
# Can cause performance problems with some drivers and GPUs.
# profile=gpu-hq
# 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
# 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.
# discourged by mpv devs and not likely to make significant difference
# hwdec=auto-copy
# hwdec-codecs=all
##################
# 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 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36" user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
# user-agent="Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36"
# user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/37.0.2062.94 Chrome/37.0.2062.94 Safari/537.36"
# user-agent="Chromium/37.0.2062.94 Chrome/37.0.2062.94 Safari/537.36"
# cache settings
#
# Use a large seekable RAM cache even for local input.
cache=yes cache=yes
#
# Use extra large RAM cache (needs cache=yes to make it useful).
demuxer-max-bytes=500M demuxer-max-bytes=500M
demuxer-max-back-bytes=100M 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 cache-pause=no
# ontop=yes
# 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
sub-font="JetBrainsMono Nerd Font" sub-font="JetBrainsMono Nerd Font"
sub-font-size=45 sub-font-size=45
# osd-font="Fluent System Icons" # osd-font="Fluent System Icons"
border=no border=no
# geometry=50%
geometry=50% autofit=50%
volume=75
volume=50 audio-spdif=ac3,dts-hd,truehd
# speed-step=0.05
audio-spdif=ac3,eac3,dts-hd,truehd
# glsl-shaders="~~/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" # glsl-shaders="~~/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"
glsl-shaders="~~/shaders/FSRCNNX.glsl:~~/shaders/FSR.glsl:~~/shaders/NVScaler.glsl:~~/shaders/CAS-scaled.glsl" # glsl-shaders="~~/shaders/FSRCNNX.glsl:~~/shaders/FSR.glsl:~~/shaders/NVScaler.glsl:~~/shaders/CAS-scaled.glsl"
# glsl-shaders="~~/shaders/FSR.glsl" # glsl-shaders="~~/shaders/ArtCNN_C4F32_DS.glsl"
glsl-shaders="~~/shaders/ArtCNN_C4F32.glsl"
# Can fix stuttering in some cases, in other cases probably causes it. Try it if you experience stuttering. # Can fix stuttering in some cases, in other cases probably causes it. Try it if you experience stuttering.
opengl-early-flush=no opengl-early-flush=no
@@ -172,46 +31,79 @@ ytdl-format=bestvideo+bestaudio/best
sub-auto=fuzzy sub-auto=fuzzy
slang=en,eng slang=en,eng
# CATPPUCCIN MACHIATTO
# Main mpv options
background-color='#24273a'
osd-back-color='#181926'
osd-border-color='#181926'
osd-color='#cad3f5'
osd-shadow-color='#24273a'
# Stats script options
# Options are on separate lines for clarity
# Colors are in #BBGGRR format
script-opts-append=stats-border_color=30201e
script-opts-append=stats-font_color=f5d3ca
script-opts-append=stats-plot_bg_border_color=f8bdb7
script-opts-append=stats-plot_bg_color=30201e
script-opts-append=stats-plot_color=f8bdb7
# profile=svp # profile=svp
profile=gpu-hq profile=high-quality
blend-subtitles=video
# GPU OPTIONS # GPU OPTIONS
vo=gpu-next vo=gpu-next
hwdec=nvdec-copy hwdec=nvdec
scale=bicubic gpu-api=vulkan
dscale=bicubic scale=ewa_lanczossharp
dscale=catmull_rom
cscale=bicubic cscale=bicubic
tscale=oversample tscale=oversample
interpolation=yes interpolation=yes
interpolation-preserve=no interpolation-preserve=no
# fruit: 8-Bit/8-Bit+FRC display
# ordered: true 10-Bit/12-Bit display
# error-diffusion: high-end GPUs
dither=error-diffusion
dither-depth=auto
error-diffusion=sierra-lite # uncomment if not 'error-diffusion'
###### Antiring
scale-antiring=0.5
dscale-antiring=0.5
cscale-antiring=0.5
# laptop # laptop
# vo=gpu # vo=gpu
# gpu-api=opengl # gpu-api=opengl
# gpu-context=wayland # gpu-context=wayland
# profile=opengl-hq # profile=opengl-hq
gpu-context=waylandvk
gpu-api=vulkan
vulkan-swap-mode=mailbox
swapchain-depth=2
vulkan-async-compute=no
vd-lavc-threads=0
opengl-pbo=yes
vd-lavc-film-grain=gpu
input-ipc-server=/tmp/mpvsocket input-ipc-server=/tmp/mpvsocket
# ao=pule,pipewire # ao=pule,pipewire
ao=pipewire,pulse ao=pipewire,pulse
deband=yes
deband-iterations=4
deband-threshold=35
deband-range=16
deband-grain=4
subs-with-matching-audio=no
sub-fix-timing=yes
sub-ass-override=scale
#Some settings fixing VOB/PGS subtitles (creating blur & changing yellow subs to gray)
sub-gauss=1.0
sub-gray=yes
###### High-quality screenshots
screenshot-format=webp
screenshot-webp-lossless=yes
screenshot-high-bit-depth=yes
screenshot-sw=no
screenshot-directory="/truenas/sudacode/pictures/mpv"
screenshot-template="%f-%wH.%wM.%wS.%wT-#%#00n"
save-position-on-quit
watch-later-dir="~~/.watch-later"
resume-playback=yes
save-watch-history
watch-history-path="~~/state/watch_history.jsonl"
target-colorspace-hint=yes
[hdr] [hdr]
target-colorspace-hint=yes target-colorspace-hint=yes
gpu-api=vulkan gpu-api=vulkan
@@ -230,14 +122,18 @@ keepaspect=no
[immersion] [immersion]
cookies=yes cookies=yes
cookies-file=/truenas/sudacode/japanese/cookies.txt cookies-file=/truenas/sudacode/japanese/cookies.Japanese.txt
ytdl-raw-options=mark-watched=,write-auto-subs=,sub-langs=ja.* ytdl-raw-options=mark-watched=,write-auto-subs=,sub-langs=ja.*
ytdl-raw-options-append=cookies=/truenas/sudacode/japanese/cookies.txt ytdl-raw-options-append=cookies=/truenas/sudacode/japanese/cookies.Japanese.txt
ytdl-raw-options-append=sponsorblock-mark=all ytdl-raw-options-append=sponsorblock-mark=all
ytdl-raw-options-append=sponsorblock-remove=sponsor ytdl-raw-options-append=sponsorblock-remove=sponsor
ytdl-format=bestvideo+bestaudio/best ytdl-format=bestvideo+bestaudio/best
# get subtitles for videos automatically # get subtitles for videos automatically
sub-auto=fuzzy sub-auto=fuzzy
slang=ja,jpn slang=ja,jpn,JA,JPN,ja.hi,ja.*
alang=ja,jpn alang=ja,jpn
vlang=ja,jpn vlang=ja,jpn
sub-font="Noto Sans CJK JP"
sub-border-size=1
sub-shadow-color=0.0/0.0/0.0/0.50
sub-shadow-offset=2

BIN
mpv_websocket-mac Executable file

Binary file not shown.

View File

@@ -0,0 +1,11 @@
# Use 'yes' or 'no' for boolean options below
# Example for multiple directories (comma or semicolon separated):
# DIRECTORIES=D:/Torrents,D:/Anime
# or
# DIRECTORIES=D:/Torrents;D:/Anime
DIRECTORIES=/truenas/jellyfin/anime
UPDATE_PERCENTAGE=85
SET_COMPLETED_TO_REWATCHING_ON_FIRST_EPISODE=no
UPDATE_PROGRESS_WHEN_REWATCHING=yes
SET_TO_COMPLETED_AFTER_LAST_EPISODE_CURRENT=yes
SET_TO_COMPLETED_AFTER_LAST_EPISODE_REWATCHING=yes

107
script-opts/animecards.conf Normal file
View File

@@ -0,0 +1,107 @@
# =================================================
# Anacreon Script (Animecards) Config for MPV
# =================================================
# ⚠ WARNING:
# Use only yes/no (not true/false) in boolean options
# =================================================
# Anki Field Names
# These must match the field names in your Anki note type
# =================================================
FRONT_FIELD=Expression
SENTENCE_FIELD=Sentence
IMAGE_FIELD=Picture
SENTENCE_AUDIO_FIELD=SentenceAudio
# =================================================
# Behavior Settings
# =================================================
# [Not recommended] Copy subtitles to clipboard enabled by default? (yes/no)
# The more modern and recommended alternative is to use the websocket.
ENABLE_SUBS_TO_CLIP=no
# Ask before overwriting existing cards? (yes/no)
# Recommended to set yes unless you know what you are doing
ASK_TO_OVERWRITE=yes
# [Dangerous] Max cards that can be overwritten at once (-1 = unlimited)
OVERWRITE_LIMIT=8
# Keep bold formatting added by yomitan? (yes/no)
HIGHLIGHT_WORD=no
# Use MPV's built-in clipboard API (requires v0.40+)? (yes/no)
# Alternative clipboard method that may reduce latency on Windows.
# Supported on macOS and Wayland as well. Not supported on X11.
USE_MPV_CLIPBOARD_API=no
# ==========================================================
# Audio Settings
# ==========================================================
# [Optional] Padding and fade settings in seconds. (0 = disable)
# Padding grabs extra audio around your selected subs.
# Fade does a volume fade in/out effect.
AUDIO_CLIP_PADDING=0.75
AUDIO_CLIP_FADE=0.2
# Always create mono audio? (yes/no)
AUDIO_MONO=yes
# [Optional] Use MPV's current volume for card audio? (yes/no)
USE_MPV_VOLUME=no
# [Optional] Play sentence audio after updating the card? (yes/no)
AUTOPLAY_AUDIO=no
# =================================================
# Image Settings
# =================================================
# Format of screenshots: png, jpg or webp
# ⚠ Use png or jpg for iOS/Mac compatibility
# (webp won't display on iOS/Mac)
IMAGE_FORMAT=png
# Resize image to this height (in pixels).
# Preserves aspect ratio. (0 = keep original resolution)
IMAGE_HEIGHT=480
# [JPG only] Quality: from 0 (worst) to 100 (best)
JPG_QUALITY=88
# =================================================
# Animated Image Settings
# =================================================
# Enable animated export for the selected subtitle segment (yes/no)
ANIMATED_IMAGE_ENABLED=yes
# Animated format: webp or avif
ANIMATED_IMAGE_FORMAT=avif
# Resize animated image to this height (in pixels). (0 = keep original)
ANIMATED_IMAGE_HEIGHT=480
# Frames per second for animated export (1-30)
ANIMATED_IMAGE_FPS=24
# Quality 0-100 (mapped to CRF for avif)
ANIMATED_IMAGE_QUALITY=69
# =================================================
# Misc Info - Extra metadata for cards
# =================================================
# [Optional] Save extra metadata (filename, timestamp, etc.) to a field
WRITE_MISCINFO=yes
# Field to store the extra info (only used if WRITE_MISCINFO=yes)
MISCINFO_FIELD=MiscInfo
# Pattern for the Misc Info content:
# %f = filename (without extension)
# %F = filename (with extension)
# %t = timestamp (HH:MM:SS)
# %T = timestamp with milliseconds (HH:MM:SS:MLS)
# <br> = Next line tag
# Examples:
# MISCINFO_PATTERN %f (%t)
# MISCINFO_PATTERN=File: %F<br>Timestamp: %T
MISCINFO_PATTERN=[Anacreon Script] %f (%t)

View File

@@ -19,13 +19,13 @@ audio_subsync_tool=ffsubsync
# 2. Preferred tool for syncing to another subtitle. # 2. Preferred tool for syncing to another subtitle.
# altsub_subsync_tool=ask # altsub_subsync_tool=ask
altsub_subsync_tool=ffsubsync # altsub_subsync_tool=ffsubsync
# altsub_subsync_tool=alass altsub_subsync_tool=alass
# Unload old subs (yes,no) # Unload old subs (yes,no)
# After retiming, tell mpv to forget the original subtitle track. # After retiming, tell mpv to forget the original subtitle track.
# unload_old_sub=yes unload_old_sub=yes
unload_old_sub=no # unload_old_sub=no
# Overwrite the original subtitle file. # Overwrite the original subtitle file.
# Replace the old subtitle file with the retimed file. # Replace the old subtitle file with the retimed file.

View File

@@ -0,0 +1,22 @@
start_tracking_key=ctrl+t
data_dir=/home/sudacode/.config/mpv/scripts/immersion-tracker/data
csv_file=/truenas/sudacode/japanese/immersion_tracker.csv
session_file=/home/sudacode/.config/mpv/scripts/immersion-tracker/data/current_session.json
min_session_duration=30
save_interval=10
enable_debug_logging=no
backup_sessions=no
max_backup_files=10
use_title=yes
use_filename=no
custom_prefix=[Immersion]
max_title_length = 100
export_csv=yes
export_json=no
export_html=no
backup_csv=yes
show_session_start=yes
show_session_end=yes
show_progress_milestones=no
milestone_percentages=25507590

View File

@@ -30,6 +30,6 @@ menu_timeout=5
show_errors=yes show_errors=yes
ytdlp_file_format=mp4 ytdlp_file_format=mp4
ytdlp_output_template=%(uploader)s/%(title)s.%(ext)s ytdlp_output_template=%(uploader)s/%(title)s.%(ext)s
use_history_db=no use_history_db=yes
backend_host=http://localhost backend_host=http://localhost
backend_port=42069 backend_port=42069

331
script-opts/subs2srs.conf Normal file
View File

@@ -0,0 +1,331 @@
###
### Main mpvacious configuration file.
### Save this file to ~/.config/mpv/script-opts/subs2srs.conf
###
##
## General settings
##
# Anki deck for new cards. Subdecks are supported.
deck_name=Minecraft
# Model names are listed in `Tools -> Manage note types` menu in Anki.
# If you don't have a model for Japanese, get it from
# https://tatsumoto.neocities.org/blog/setting-up-anki.html#import-an-example-mining-deck
model_name=Lapis
# Field names as they appear in the selected note type.
# If you set `audio_field` or `image_field` empty,
# the corresponding media file will not be created.
audio_field=SentenceAudio
image_field=Picture
sentence_field=Expression
secondary_field=SelectionText
# The tag(s) added to new notes. Spaces separate multiple tags.
# Leave nothing after `=` to disable tagging completely.
# The following substitutions are supported:
# %n - the name of the video
# %t - timestamp
# %d - episode number (if none, returns nothing)
# %e - SUBS2SRS_TAGS environment variable (if you have it set)
# note_tag=subs2srs
note_tag=%n-E%d %t
#note_tag=
# Size and name of the font used in the menu
menu_font_size=24
menu_font_name=Noto Serif CJK JP
# AnkiConnect server address
# The default address for a server on the same device is http://127.0.0.1:8765.
# If Anki is running and AnkiConnect is installed, opening this URL should
# open a page showing the current version of AnkiConnect.
# Change this if you have changed webBindAddress in AnkiConnect's settings.
ankiconnect_url=127.0.0.1:8765
##
## Toggleables.
## Possible values: `yes` or `no`.
# Use FFmpeg encoder instead of mpv encoder
# If mpvacious encounters problems creating audio and images for Anki cards,
# setting this to `yes` should fix them.
#
# You need to install ffmpeg and add it to the PATH first.
# https://wiki.archlinux.org/title/FFmpeg
# https://www.ffmpeg.org/download.html
#
# FFmpeg encoder is unable to create audio and images from remote content (like YouTube videos).
use_ffmpeg=yes
# Automatically create the deck for new cards (see deck_name option)
create_deck=no
# Allow making notes with the same sentence field.
allow_duplicates=no
# When mpv starts, automatically copy subs to the clipboard as they appear on screen.
# This option can be also toggled in the addon's OSD menu.
autoclip=no
# Possible options:
# "disabled" - autocopy is disabled
# "clipboard" - copy to the system clipboard (e.g. uses xclip)
# "goldendict" - send the subtitle string to goldendict (goldendict-ng).
# "custom_command" - run any custom command specified in `autoclip_custom_args`.
autoclip_method=custom_command
# Command to run when autoclip is enabled and set to "custom_command".
# If empty, nothing will be done.
# If set, calls the external program.
# The following substitutions are supported:
# %MPV_PRIMARY% - primary subtitle line
# %MPV_SECONDARY% - secondary subtitle line
autoclip_custom_args=wl-copy %MPV_PRIMARY%
# Remove all spaces from the primary subtitle text.
# Set this to "yes" for languages without spaces like Japanese.
# However, if mpvacious detects any latin characters in the string, spaces will not be removed.
nuke_spaces=yes
# if set to `yes`, the volume of the outputted audio file
# depends on the volume of the player at the time of export
tie_volumes=no
# This is used when selecting cards in Anki to update, it wont let you
# overwrite more than the value specified below
# Just remember that having multiple cards with the same sentence
# and the same audio recording is usually bad practice.
card_overwrite_safeguard = 1
# Remove text in parentheses and leading/trailing spaces or
# newlines that may interfere with Rikaitan before copying
# subtitles to the clipboard
clipboard_trim_enabled=yes
# Add media to fields before or after existing data
append_media=yes
# Remove text in brackets before substituting %n into tag
tag_nuke_brackets=yes
# Remove text in parentheses before substituting %n into tag
tag_nuke_parentheses=no
# Remove the episode number before substituting %n into tag
tag_del_episode_num=yes
# Remove everything after the episode number before substituting %n into tag
# Does nothing if the previous option tag_del_episode_num is disabled.
tag_del_after_episode_num=yes
# Convert filename to lowercase for tagging.
tag_filename_lowercase=no
# Lets you disable anki browser manipulation by mpvacious.
disable_gui_browse=no
# Play audio clip automatically in background
# after note creation (or note update) to ensure that the audio is correctly cut.
preview_audio=yes
# When selecting subtitle lines, print them on the screen.
show_selected_text=yes
# For convenience, read config file from disk before a card is made.
# Useful if you change your config often since you won't have to restart mpv every time,
# but reading from disk takes some time.
reload_config_before_card_creation=yes
##
## Image settings
##
# Snapshot format.
# Do not switch to `jpg` unless your computer doesn't support `webp` or `avif`.
snapshot_format=avif
# snapshot_format=webp
#snapshot_format=jpg
# Quality of produced image files. 0 = lowest, 100=highest.
snapshot_quality=88
# Image dimensions
# If either (but not both) of the width or height parameters is -2,
# the value will be calculated preserving the aspect-ratio.
snapshot_width=-2
snapshot_height=400
# Screenshot (yes, no)
# Usually not required.
# When making Anki cards, create a screenshot (by calling 'screenshot-to-file') instead of a snapshot.
# If set to yes, image dimensions and quality cannot be controlled due to mpv limitations.
# 'snapshot_format' is still respected.
# When using this, a custom sync server is recommended, e.g. https://github.com/ankicommunity/anki-sync-server
screenshot=yes
# The exact image template used when exporting to Anki's image field.
# Adding data-editor-shrink="true" makes the image smaller by default within the Anki viewer
# on versions 2.1.53+ (equivalent of double-clicking on the image).
# You likely would not want to change this unless you know what you are doing.
image_template=<img alt="snapshot" src="%s">
#image_template=<img alt="snapshot" data-editor-shrink="true" src="%s">
# Similar to image_template but with audio.
# Normally, the user doesn't need to change this setting,
# but it may be needed for audio files to be playable on AnkiWeb.
audio_template=[sound:%s]
#audio_template=<audio controls="" src="%s"></audio>
##
## Animated snapshots
## Animated snapshots will capture the video from the start to the end times selected when using mpvacious.
##
# If enabled, generates animated snapshots (something like GIFs) instead of static snapshots.
animated_snapshot_enabled=yes
# Animated snapshot format. Like "snapshot_format" but for animated images. Can be either avif or webp.
animated_snapshot_format=avif
# animated_snapshot_format=webp
# Number of frame per seconds, a value between 0 and 30 (30 included)
# Higher values will increase both quality and file size, lower values will do the opposite
animated_snapshot_fps=24
# Animated snapshot dimensions
# If either (but not both) of the width or height parameters is -2,
# the value will be calculated preserving the aspect-ratio.
animated_snapshot_width=-2
animated_snapshot_height=400
# Quality of the produced animation, 0 = lowest, 100 = highest
animated_snapshot_quality=69
##
## Audio settings
##
# Audio format.
# Opus is the recommended format.
audio_format=opus
# audio_format=mp3
# Container for opus files.
# It may be required to use a different container for Opus.
# This is the case on certain computers or devices
# which are running proprietary operating systems, e.g. AnkiMobile. Using them is discouraged.
# ・ Ogg/Opus play everywhere except AnkiWeb in Safari and AnkiMobile.
# ・ M4A (iOS 17.2 and probably even earlier) and WEBM (since iOS 17.4) play everywhere.
# ・ Opus in CAF can be used with older iOS. CAF plays only on Anki Desktop, Safari and AnkiMobile.
# ・ (iOS Lockdown Mode disables Opus support completely,
# though you may try to add an exception for AnkiMobile.)
# opus_container=ogg
#opus_container=opus
opus_container=m4a
# opus_container=webm
#opus_container=caf
# Sane values are 16k-32k for opus, 64k-128k for mp3.
audio_bitrate=32k
# Set a pad to the dialog timings. 0.5 = half a second.
# Pads are never applied to manually set timings.
audio_padding=0.0
#audio_padding=0.5
##
## Forvo support (Rikaitan users only)
##
# yes - fetch audio from Forvo if Rikaitan couldn't find the audio (default)
# always - always fetch audio from Forvo and replace the audio added by Rikaitan
# no - never use Forvo
use_forvo=yes
# Vocab field should be equal to {expression} field in Rikaitan
vocab_field=Expression
# Vocab Audio field should be equal to {audio} field in Rikaitan
vocab_audio_field=ExpressionAudio
##
## Misc info
## Various context information that can be written on your cards in a specified field.
##
# yes to enable or no to disable.
miscinfo_enable=yes
# Field name
miscinfo_field=ExtraInfo
# Format string used to fill the misc info field.
# It supports the same substitutions as `note_tag`. HTML is supported.
miscinfo_format=%n EP%d (%t)
#miscinfo_format=From <b>mpvacious</b> %n at %t.
##
## Secondary subtitles
## Mpvacious can try automatically loading secondary subtitles that will appear at the top.
## For example, you may want to load English subs alongside Japanese subs.
##
## Secondary subtitles should be present in the container.
## But if you manually set secondary sid from the command line, mpvacious won't change it.
##
# Load secondary subtitle track automatically when a file is opened.
secondary_sub_auto_load=yes
# Language of secondary subs. This is your native language or a language you know well.
# If you leave this parameter empty, secondary subs will NOT be automatically loaded.
secondary_sub_lang=eng,en
#secondary_sub_lang=
# Hover area.
# Proportion of the top part of the mpv window where the secondary subtitles are visible when hovered over.
# Possible values: from 0.0 to 1.0
secondary_sub_area=0.15
# Visibility state
# Can be set to: 'auto', 'never', 'always'.
# If set to 'never' or 'always', secondary_sub_area has no effect.
# If set to 'auto', visibility behaves according to the value of secondary_sub_area.
# Default binding to cycle this value: Ctrl+v.
secondary_sub_visibility=auto
# Perform two-pass loudness normalization.
# Parameter explanation can be found e.g. at:
# https://auphonic.com/blog/2013/01/07/loudness-targets-mobile-audio-podcasts-radio-tv/
# https://auphonic.com/blog/2019/08/19/dynamic-range-processing/
# MAKE SURE TO REMOVE loudnorm FROM CUSTOM ARGS BEFORE ENABLING.
loudnorm=no
loudnorm_target=-16
loudnorm_range=11
loudnorm_peak=-1.5
##
## Custom audio encoding arguments
## These arguments are added to the command line.
## `mpv` and `ffmpeg` accept slightly different parameters.
## Feel free to experiment for yourself, but be careful or media creation might stop working.
##
# loudnorm IN CUSTOM ARGS IS LEFT FOR BACKWARD COMPATIBILITY.
# MAKE SURE TO REMOVE ALL MENTIONS OF loudnorm FROM CUSTOM ARGS
# (E.G. SET TO EMPTY STRINGS) BEFORE ENABLING TWO-PASS loudnorm.
# ENABLING loudnorm BOTH THROUGH THE SWITCH AND THROUGH CUSTOM ARGS
# CAN LEAD TO UNPREDICTABLE RESULTS.
# Ffmpeg
ffmpeg_audio_args=-af loudnorm=I=-16:TP=-1.5:LRA=11:dual_mono=true
#ffmpeg_audio_args=
#ffmpeg_audio_args=-af silenceremove=1:0:-50dB
# mpv
# mpv accepts each filter as a separate argument, e.g. --af-append=1 --af-append=2
mpv_audio_args=--af-append=loudnorm=I=-16:TP=-1.5:LRA=11:dual_mono=true
#mpv_audio_args=
#mpv_audio_args=--af-append=silenceremove=1:0:-50dB

View File

@@ -0,0 +1 @@
temp=/tmp/ytdl-preload

View File

@@ -1,568 +0,0 @@
import ast
import hashlib
import os
import re
import sys
import time
import webbrowser
import requests
from guessit import guessit
class AniListUpdater:
ANILIST_API_URL = "https://graphql.anilist.co"
TOKEN_PATH = os.path.join(os.path.dirname(__file__), "anilistToken.txt")
OPTIONS = "--excludes country --excludes language --type episode"
CACHE_REFRESH_RATE = 24 * 60 * 60
# Load token and user id
def __init__(self):
self.access_token = (
self.load_access_token()
) # Replace token here if you don't use the .txt
self.user_id = self.get_user_id()
# Load token from anilistToken.txt
def load_access_token(self):
try:
with open(self.TOKEN_PATH, "r") as file:
content = file.read().strip()
if ":" in content:
token = content.split(":", 1)[1].splitlines()[0]
return token
return content
except Exception as e:
print(f"Error reading access token: {e}")
return None
# Load user id from file, if not then make api request and save it.
def get_user_id(self):
try:
with open(self.TOKEN_PATH, "r") as file:
content = file.read().strip()
if ":" in content:
return int(content.split(":")[0])
except Exception as e:
print(f"Error reading user ID: {e}")
query = """
query {
Viewer {
id
}
}
"""
response = self.make_api_request(query, None, self.access_token)
if response and "data" in response:
user_id = response["data"]["Viewer"]["id"]
self.save_user_id(user_id)
return user_id
return None
# Cache user id
def save_user_id(self, user_id):
try:
with open(self.TOKEN_PATH, "r+") as file:
content = file.read()
file.seek(0)
file.write(f"{user_id}:{content}")
except Exception as e:
print(f"Error saving user ID: {e}")
def cache_to_file(self, path, guessed_name, result):
try:
with open(self.TOKEN_PATH, "a") as file:
# Epoch Time, hash of the path, guessed name, result
file.write(
f"\n{time.time()};;{self.hash_path(os.path.dirname(path))};;{guessed_name};;{result}"
)
except Exception as e:
print(f"Error trying to cache {result}: {e}")
def hash_path(self, path):
return hashlib.sha256(path.encode("utf-8")).hexdigest()
def check_and_clean_cache(self, path, guessed_name):
try:
valid_lines = []
unique = set()
path = self.hash_path(os.path.dirname(path))
cached_result = (None, None)
with open(self.TOKEN_PATH, "r+") as file:
orig_lines = file.readlines()
for line in orig_lines:
if line.strip():
if ";;" in line:
epoch, dir_path, guess, result = line.strip().split(";;")
if (
time.time() - float(epoch) < self.CACHE_REFRESH_RATE
and (dir_path, guess) not in unique
):
unique.add((dir_path, guess))
valid_lines.append(line)
if dir_path == path and guess == guessed_name:
cached_result = (result, len(valid_lines) - 1)
else:
valid_lines.append(line)
if valid_lines != orig_lines:
with open(self.TOKEN_PATH, "w") as file:
file.writelines(valid_lines)
return cached_result
except Exception as e:
print(f"Error trying to read cache file: {e}")
def update_cache(self, path, guessed_name, result, index):
try:
with open(self.TOKEN_PATH, "r") as file:
lines = file.readlines()
if 0 <= index < len(lines):
# Update the line at the given index with the new cache data
updated_line = (
f"{time.time()};;{self.hash_path(os.path.dirname(path))};;{guessed_name};;{result}\n"
if result is not None
else ""
)
lines[index] = updated_line
# Write the updated lines back to the file
with open(self.TOKEN_PATH, "w") as file:
file.writelines(lines)
else:
print(f"Invalid index {index} for updating cache.")
except Exception as e:
print(f"Error trying to update cache file: {e}")
# Function to make an api request to AniList's api
def make_api_request(self, query, variables=None, access_token=None):
headers = {"Content-Type": "application/json", "Accept": "application/json"}
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
response = requests.post(
self.ANILIST_API_URL,
json={"query": query, "variables": variables},
headers=headers,
)
# print(f"Made an API Query with: Query: {query}\nVariables: {variables} ")
if response.status_code == 200:
return response.json()
else:
print(
f"API request failed: {response.status_code} - {response.text}\nQuery: {query}\nVariables: {variables}"
)
return None
@staticmethod
def season_order(season):
return {"WINTER": 1, "SPRING": 2, "SUMMER": 3, "FALL": 4}.get(season, 5)
def filter_valid_seasons(self, seasons):
# Filter only to those whose format is TV and duration > 21 OR those who have no duration and are releasing.
# This is due to newly added anime having duration as null
seasons = [
season
for season in seasons
if (
(season["duration"] is None and season["status"] == "RELEASING")
or (season["duration"] is not None and season["duration"] > 21)
)
and season["format"] == "TV"
]
# One of the problems with this filter is needing the format to be 'TV'
# But if accepted any format, it would also include many ONA's which arent included in absolute numbering.
# Sort them based on release date
seasons = sorted(
seasons,
key=lambda x: (
x["seasonYear"] if x["seasonYear"] else float("inf"),
self.season_order(x["season"] if x["season"] else float("inf")),
),
)
return seasons
# Finds the season and episode of an anime with absolute numbering
def find_season_and_episode(self, seasons, absolute_episode):
accumulated_episodes = 0
for season in seasons:
season_episodes = (
season.get("episodes", 12) if season.get("episodes") else 12
)
if accumulated_episodes + season_episodes >= absolute_episode:
return (
season.get("id"),
season.get("title", {}).get("romaji"),
(
season.get("mediaListEntry", {}).get("progress")
if season.get("mediaListEntry")
else None
),
season.get("episodes"),
absolute_episode - accumulated_episodes,
)
accumulated_episodes += season_episodes
return (None, None, None, None, None)
def handle_filename(self, filename):
file_info = self.parse_filename(filename)
cached_result, line_index = self.check_and_clean_cache(
filename, file_info.get("name")
)
# str -> tuple
cached_result = ast.literal_eval(cached_result) if cached_result else None
# True if:
# Is not cached
# Tries to update and current episode is not the next one.
# It is not in your watching/planning list.
# This means that for shows with absolute numbering, if it updates, it will always call the API
# Since it needs to convert from absolute to relative.
if cached_result is None or (
cached_result
and (file_info.get("episode") != cached_result[2] + 1)
and sys.argv[2] != "launch"
):
result = self.get_anime_info_and_progress(
file_info.get("name"), file_info.get("episode"), file_info.get("year")
)
result = self.update_episode_count(
result
) # Returns either the same, or the updated result
# If it returned a result and the progress isnt None, then put it in cache, since it wasn't.
if result and result[2] is not None:
if line_index is not None:
print(f"Updating cache to: {result}")
self.update_cache(
filename, file_info.get("name"), result, line_index
)
else:
print(f"Not found in cache! Adding to file... {result}")
self.cache_to_file(filename, file_info.get("name"), result)
# True for opening AniList and updating next episode.
else:
print(f"Found in cache! {cached_result}")
# Change to the episode that needs to be updated
cached_result = cached_result[:4] + (file_info.get("episode"),)
result = self.update_episode_count(cached_result)
# If it's different, update in cache as well.
if cached_result != result and result:
print(f"Updating cache to: {result}")
self.update_cache(filename, file_info.get("name"), result, line_index)
# If it either errored or couldn't update, retry without cache.
if not result:
print(f"Failed to update through cache, retrying without.")
# Deleting from the cache
self.update_cache(filename, file_info.get("name"), None, line_index)
# Retrying
self.handle_filename(filename)
return
# Hardcoded exceptions to fix detection
# Easier than just renaming my files 1 by 1 on Qbit
# Every exception I find will be added here
def fix_filename(self, path_parts):
guess = guessit(
path_parts[-1], self.OPTIONS
) # Simply easier for fixing the filename if we have what it is detecting.
path_parts[-1] = os.path.splitext(path_parts[-1])[0]
pattern = r'[\\\/:!\*\?"<>\|\._-]'
title_depth = -1
# Fix from folders if the everything is not in the filename
if "title" not in guess:
# Depth=2
for depth in range(2, min(4, len(path_parts))):
folder_guess = guessit(path_parts[-depth], self.OPTIONS)
if "title" in folder_guess:
guess["title"] = folder_guess["title"]
title_depth = -depth
break
if "title" not in guess:
print(
f"Couldn't find title in filename '{path_parts[-1]}'! Guess result: {guess}"
)
return path_parts
# Only clean up titles for some series
cleanup_titles = ["Ranma", "Chi", "Bleach"]
if any(title in guess["title"] for title in cleanup_titles):
path_parts[title_depth] = re.sub(pattern, " ", path_parts[title_depth])
path_parts[title_depth] = " ".join(path_parts[title_depth].split())
if "Centimeters per Second" == guess["title"] and 5 == guess.get("episode", 0):
path_parts[title_depth] = path_parts[title_depth].replace(" 5 ", " Five ")
# For some reason AniList has this film in 3 parts.
path_parts[title_depth] = path_parts[title_depth].replace(
"per Second", "per Second 3"
)
return path_parts
# Parse the file name using guessit
def parse_filename(self, filepath):
path_parts = self.fix_filename(filepath.replace("\\", "/").split("/"))
filename = path_parts[-1]
name, season, part, year = "", "", "", ""
episode = 1
# First, try to guess from the filename
guess = guessit(filename, self.OPTIONS)
print(f"File name guess: {filename} -> {dict(guess)}")
# Episode guess from the title.
# Usually, releases are formated [Release Group] Title - S01EX
# If the episode index is 0, that would mean that the episode is before the title in the filename
# Which is a horrible way of formatting it, so assume its wrong
# If its 1, then the title is probably 0, so its okay. (Unless season is 0)
# Really? What is the format "S1E1 - {title}"? That's almost psycopathic.
# If its >2, theres probably a Release Group and Title / Season / Part, so its good
episode = guess.get("episode", None)
season = guess.get("season", "")
part = str(guess.get("part", ""))
year = str(guess.get("year", ""))
# Quick fixes assuming season before episode
# 'episode_title': '02' in 'S2 02'
if guess.get("episode_title", "").isdigit() and episode is None:
print(
f'Detected episode in episode_title. Episode: {int(guess.get("episode_title"))}'
)
episode = int(guess.get("episode_title"))
# 'episode': [86, 13] (EIGHTY-SIX), [1, 2, 3] (RANMA) lol.
if isinstance(episode, list):
print(f"Detected multiple episodes: {episode}. Picking last one.")
episode = episode[-1]
# 'season': [2, 3] in "S2 03"
if isinstance(season, list):
print(f"Detected multiple seasons: {season}. Picking first one as season.")
if episode is None:
print(
"Episode still not detected. Picking last position of the season list."
)
episode = season[-1]
season = season[0]
episode = episode or 1
season = str(season)
keys = list(guess.keys())
episode_index = keys.index("episode") if "episode" in guess else 1
season_index = keys.index("season") if "season" in guess else -1
title_in_filename = "title" in guess and (
episode_index > 0 and (season_index > 0 or season_index == -1)
)
# If the title is not in the filename or episode index is 0, try the folder name
# If the episode index > 0 and season index > 0, its safe to assume that the title is in the file name
if title_in_filename:
name = guess["title"]
else:
# If it isnt in the name of the file, try to guess using the name of the folder it is stored in
# Depth=2 folders
for depth in [2, 3]:
folder_guess = (
guessit(path_parts[-depth], self.OPTIONS)
if len(path_parts) > depth - 1
else ""
)
if folder_guess != "":
print(
f'{depth-1}{"st" if depth-1==1 else "nd"} Folder guess:\n{path_parts[-depth]} -> {dict(folder_guess)}'
)
name = str(folder_guess.get("title", ""))
season = season or str(folder_guess.get("season", ""))
part = part or str(folder_guess.get("part", ""))
year = year or str(folder_guess.get("year", ""))
if name != "":
break # If we got the name, its probable we already got season and part from the way folders are usually structured
# Add season and part if there are
if season and (int(season) > 1 or part):
name += f" Season {season}"
if part:
name += f" Part {part}"
print("Guessed name: " + name)
return {
"name": name,
"episode": episode,
"year": year,
}
def get_anime_info_and_progress(self, name, file_progress, year):
query = """
query($search: String, $year: FuzzyDateInt, $page: Int) {
Page(page: $page) {
media (search: $search, type: ANIME, startDate_greater: $year) {
id
title { romaji }
season
seasonYear
episodes
duration
format
status
mediaListEntry {
status
progress
media {
episodes
}
}
}
}
}
"""
variables = {"search": name, "year": year or 1, "page": 1}
response = self.make_api_request(query, variables, self.access_token)
if response and "data" in response:
seasons = response["data"]["Page"]["media"]
# This is the first element, which is the same as Media(search: $search)
if len(seasons) == 0:
raise Exception(f"Couldn't find an anime from this title! ({name})")
anime_data = (
seasons[0]["id"],
seasons[0]["title"]["romaji"],
(
seasons[0]["mediaListEntry"]["progress"]
if seasons[0]["mediaListEntry"] is not None
else None
),
seasons[0]["episodes"],
file_progress,
)
# If the episode in the file name is larger than the total amount of episodes
# Then they are using absolute numbering format for episodes (looking at you SubsPlease)
# Try to guess season and episode.
if (
seasons[0]["episodes"] is not None
and file_progress > seasons[0]["episodes"]
):
seasons = self.filter_valid_seasons(seasons)
print(
"Related shows:",
", ".join(season["title"]["romaji"] for season in seasons),
)
anime_data = self.find_season_and_episode(seasons, file_progress)
print(
f"Final guessed anime: {next(season for season in seasons if season['id'] == anime_data[0])}"
) # Print data of the show
print(
f"Absolute episode {file_progress} corresponds to Anime: {anime_data[1]}, Episode: {anime_data[-1]}"
)
else:
print(f"Final guessed anime: {seasons[0]}") # Print data of the show
return anime_data
return (None, None, None, None)
# Update the anime based on file progress
def update_episode_count(self, result):
if result is None:
raise Exception("Parameter in update_episode_count is null.")
anime_id, anime_name, current_progress, total_episodes, file_progress = result
# Only launch anilist
if sys.argv[2] == "launch":
print(
f'Opening AniList for "{anime_name}": https://anilist.co/anime/{anime_id}'
)
webbrowser.open_new_tab(f"https://anilist.co/anime/{anime_id}")
return result
if current_progress is None:
raise Exception(
"Failed to get current episode count. Is it on your watching/planning list?"
)
# If its lower than the current progress, dont update.
if file_progress <= current_progress:
raise Exception(
f"Episode was not new. Not updating ({file_progress} <= {current_progress})"
)
query = """
mutation ($mediaId: Int, $progress: Int, $status: MediaListStatus) {
SaveMediaListEntry (mediaId: $mediaId, progress: $progress, status: $status) {
status
id
progress
}
}
"""
variables = {"mediaId": anime_id, "progress": file_progress}
# Handle changing "Planned to watch" animes to "Watching"
if file_progress != total_episodes:
variables["status"] = (
"CURRENT" # Set to "CURRENT" if it isn't the final episode.
)
response = self.make_api_request(query, variables, self.access_token)
if response and "data" in response:
updated_progress = response["data"]["SaveMediaListEntry"]["progress"]
print(
f"Episode count updated successfully! New progress: {updated_progress}"
)
return (
anime_id,
anime_name,
updated_progress,
total_episodes,
file_progress,
)
else:
print("Failed to update episode count.")
return False
def main():
try:
updater = AniListUpdater()
updater.handle_filename(sys.argv[1])
except Exception as e:
print(f"ERROR: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
../../submodules/mpv-anilist-updater/anilistUpdater/anilistUpdater.py

View File

@@ -1,129 +0,0 @@
local utils = require("mp.utils")
-- The directory the script will work on
-- Leaving it blank will make it work on every video you watch with mpv
-- You can still update manually via Ctrl+A
-- Setting a directory will only work if the path of the video contains this directory
DIRECTORY = ""
-- Example: DIRECTORY = "D:/Torrents" or "D:/Anime"
function callback(success, result, error)
if result.status == 0 then
mp.osd_message("Updated anime correctly.", 2)
end
end
local function get_python_command()
local os_name = package.config:sub(1, 1)
if os_name == "\\" then
-- Windows
return "python"
else
-- Linux
return "python3"
end
end
local python_command = get_python_command()
-- Make sure it doesnt trigger twice in 1 video
local triggered = false
-- Function to check if we've reached 85% of the video
function check_progress()
if triggered then
return
end
local percent_pos = mp.get_property_number("percent-pos")
if percent_pos then
if percent_pos >= 85 then
update_anilist("update")
triggered = true
end
end
end
-- Function to launch the .py script
function update_anilist(action)
if action == "launch" then
mp.osd_message("Launching AniList", 2)
end
local script_dir = debug.getinfo(1).source:match("@?(.*/)")
local directory = mp.get_property("working-directory")
-- It seems like in Linux working-directory sometimes returns it without a "/" at the end
directory = (directory:sub(-1) == "/" or directory:sub(-1) == "\\") and directory or directory .. "/"
-- For some reason, "path" sometimes returns the absolute path, sometimes it doesn't.
local file_path = mp.get_property("path")
local path = utils.join_path(directory, file_path)
local table = {}
table.name = "subprocess"
table.args = { python_command, script_dir .. "anilistUpdater.py", path, action }
local cmd = mp.command_native_async(table, callback)
end
mp.observe_property("percent-pos", "number", check_progress)
-- Reset triggered
mp.register_event("file-loaded", function()
triggered = false
if DIRECTORY ~= "" then
local directory = mp.get_property("working-directory")
directory = (directory:sub(-1) == "/" or directory:sub(-1) == "\\") and directory or directory .. "/"
local file_path = mp.get_property("path")
local path = utils.join_path(directory, file_path)
path = path:gsub("\\", "/")
if string.find(path, DIRECTORY) ~= 1 then
mp.unobserve_property(check_progress)
end
end
end)
-- Keybinds, modify as you please
mp.add_key_binding("ctrl+a", "update_anilist", function()
update_anilist("update")
end)
mp.add_key_binding("ctrl+b", "launch_anilist", function()
update_anilist("launch")
end)
-- Open the folder that the video is
function open_folder()
local path = mp.get_property("path")
local directory
if not path then
mp.msg.warn("No file is currently playing.")
return
end
if path:find("\\") then
directory = path:match("(.*)\\")
elseif path:find("\\\\") then
directory = path:match("(.*)\\\\")
else
directory = mp.get_property("working-directory")
end
-- Use the system command to open the folder in File Explorer
local args
if package.config:sub(1, 1) == "\\" then
-- Windows
args = { "explorer", directory }
elseif os.getenv("XDG_CURRENT_DESKTOP") or os.getenv("WAYLAND_DISPLAY") or os.getenv("DISPLAY") then
-- Linux (assume a desktop environment like GNOME, KDE, etc.)
args = { "xdg-open", directory }
elseif package.config:sub(1, 1) == "/" then
-- macOS
args = { "open", directory }
end
mp.command_native({ name = "subprocess", args = args, detach = true })
end
mp.add_key_binding("ctrl+d", "open_folder", open_folder)

View File

@@ -0,0 +1 @@
../../submodules/mpv-anilist-updater/anilistUpdater/main.lua

1
scripts/animecards Symbolic link
View File

@@ -0,0 +1 @@
../submodules/animecards/animecards

View File

@@ -1,496 +0,0 @@
------------- Instructions -------------
-- -- Video Demonstration: https://www.youtube.com/watch?v=M4t7HYS73ZQ
-- IF USING WEBSOCKET (RECOMMENDED)
-- -- Install the mpv_webscoket extension: https://github.com/kuroahna/mpv_websocket
-- -- Open a LOCAL copy of https://github.com/Renji-XD/texthooker-ui
-- -- Configure the script (if you're not using the Lapis note format)
-- IF USING CLIPBOARD INSERTER (NOT RECOMMENDED)
-- -- Install the clipboard inserter plugin: https://github.com/laplus-sadness/lap-clipboard-inserter
-- -- Open the texthooker UI, enable the plugin and enable clipboard pasting: https://github.com/Renji-XD/texthooker-ui
-- BOTH
-- -- Wait for an unknown word and create the card with Yomichan.
-- -- Select all the subtitle lines you wish to add to the card and copy with Ctrl + c.
-- -- Press Ctrl + v in MPV to add the lines, their Audio and the currently paused image to the back of the card.
---------------------------------------
------------- Credits -------------
-- Credits and copyright go to Anacreon DJT: https://anacreondjt.gitlab.io/
------------------------------------
------------- Original Credits (Outdated) -------------
-- This script was made by users of 4chan's Daily Japanese Thread (DJT) on /jp/
-- More information can be found here http://animecards.site/
-- Message @Anacreon with bug reports and feature requests on Discord (https://animecards.site/discord/) or 4chan (https://boards.4channel.org/jp/#s=djt)
--
-- If you like this work please consider subscribing on Patreon!
-- https://www.patreon.com/Quizmaster
------------------------------------
local utils = require("mp.utils")
local msg = require("mp.msg")
------------- User Config -------------
-- Set these to match your field names in Anki
local FRONT_FIELD = "Expression"
local SENTENCE_AUDIO_FIELD = "SentenceAudio"
local SENTENCE_FIELD = "Sentence"
local IMAGE_FIELD = "Picture"
-- Optional padding and fade settings in seconds.
-- Padding grabs extra audio around your selected subs.
-- Fade does a volume fade effect at the beginning and end of the resulting audio.
local AUDIO_CLIP_FADE = 0.2
local AUDIO_CLIP_PADDING = 0.75
-- Optional play sentence audio automatically after card update
local AUTOPLAY_AUDIO = false
-- Optional screenshot image format. Valid options: "webp" or "png"
-- Change to "png" if you plan to view cards on iOS or Mac.
local IMAGE_FORMAT = "png"
-- Optional set to true if you want your volume in mpv to affect Anki card volume.
local USE_MPV_VOLUME = false
-- Set to true if you want writing to clipboard to be enabled by default.
-- The more modern and recommended alternative is to use the websocket.
local ENABLE_SUBS_TO_CLIP = false
---------------------------------------
------------- Internal Variables -------------
local subs = {}
local debug_mode = false
local use_powershell_clipboard = nil
local prefix = ""
---------------------------------------
------------- Setup -------------
if unpack ~= nil then
table.unpack = unpack
end
local o = {}
-- Possible platforms: windows, linux, macos
local platform = mp.get_property_native("platform")
if platform == "darwin" then
platform = "macos"
end
local display_server
if os.getenv("WAYLAND_DISPLAY") then
display_server = "wayland"
elseif platform == "linux" then
display_server = "xorg"
else
display_server = ""
end
local function dlog(...)
if debug_mode then
print(...)
end
end
local function verfiy_libmp3lame()
local encoderlist = mp.get_property("encoder-list")
if not encoderlist or not string.find(encoderlist, "libmp3lame") then
mp.osd_message(
"Error: libmp3lame encoder not found. Audio export will not work.\nPlease use a build of mpv with libmp3lame support.",
10
)
msg.error("Error: libmp3lame encoder not found. MP3 audio export will not work.")
else
dlog("libmp3lame encoder found.")
end
end
mp.register_event("file-loaded", verfiy_libmp3lame)
dlog("Detected Platform: " .. platform)
dlog("Detected display server: " .. display_server)
---------------------------------------
-- Handle requests to AnkiConnect
local function anki_connect(action, params)
local request = utils.format_json({ action = action, params = params, version = 6 })
local args = { "curl", "-s", "localhost:8765", "-X", "POST", "-d", request }
dlog("AnkiConnect request: " .. request)
local result = utils.subprocess({ args = args, cancellable = false, capture_stderr = true })
if result.status ~= 0 then
msg.error("Curl command failed with status: " .. tostring(result.status))
msg.error("Stderr: " .. (result.stderr or "none"))
return nil
end
if not result.stdout or result.stdout == "" then
msg.error("Empty response from AnkiConnect")
return nil
end
dlog("AnkiConnect response: " .. result.stdout)
local success, parsed_result = pcall(function()
return utils.parse_json(result.stdout)
end)
if not success or not parsed_result then
msg.error("Failed to parse JSON response: " .. (result.stdout or "empty"))
return nil
end
return parsed_result
end
-- Get media directory path from AnkiConnect
local function set_media_dir()
local media_dir_response = anki_connect("getMediaDirPath")
if not media_dir_response then
msg.error("Failed to communicate with AnkiConnect. Is Anki running and do you have AnkiConnect installed?")
mp.osd_message(
"Error: Failed to communicate with AnkiConnect. Is Anki running and do you have AnkiConnect installed?",
5
)
return
elseif media_dir_response["error"] then
msg.error("AnkiConnect error: " .. tostring(media_dir_response["error"]))
mp.osd_message("AnkiConnect error: " .. tostring(media_dir_response["error"]), 5)
return
elseif media_dir_response["result"] then
prefix = media_dir_response["result"]
dlog("Got media directory path from AnkiConnect: " .. prefix)
else
msg.error("Unexpected response format from AnkiConnect")
mp.osd_message("Error: Unexpected response from AnkiConnect", 5)
return
end
end
local function clean(s)
for _, ws in ipairs({
"%s",
" ",
"",
" ",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
" ",
"",
"",
}) do
s = s:gsub(ws .. "+", "")
end
return s
end
local function get_name(s, e)
return mp.get_property("filename"):gsub("%W", "") .. tostring(s) .. tostring(e)
end
local function get_clipboard()
local res
if platform == "windows" then
res = utils.subprocess({
args = {
"powershell",
"-NoProfile",
"-Command",
[[& {
Trap {
Write-Error -ErrorRecord $_
Exit 1
}
$clip = ""
if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
$clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
} else {
Add-Type -AssemblyName PresentationCore
$clip = [Windows.Clipboard]::GetText()
}
$clip = $clip -Replace "`r",""
$u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
[Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
}]],
},
})
elseif platform == "macos" then
return io.popen("LANG=en_US.UTF-8 pbpaste"):read("*a")
else -- platform == 'linux'
if display_server == "wayland" then
res = utils.subprocess({ args = {
"wl-paste",
} })
else -- display_server == 'xorg'
res = utils.subprocess({ args = {
"xclip",
"-selection",
"clipboard",
"-out",
} })
end
end
if not res.error then
return res.stdout
end
end
local function powershell_set_clipboard(text)
utils.subprocess({
args = {
"powershell",
"-NoProfile",
"-Command",
[[Set-Clipboard -Value @"]] .. "\n" .. text .. "\n" .. [["@]],
},
})
end
local function cmd_set_clipboard(text)
local cmd = "echo " .. text .. " | clip"
mp.command("run cmd /D /C " .. cmd)
end
local function determine_clip_type()
powershell_set_clipboard([[Anacreon様]])
use_powershell_clipboard = get_clipboard() == [[Anacreon様]]
end
local function linux_set_clipboard(text)
if display_server == "wayland" then
os.execute("wl-copy <<EOF\n" .. text .. "\nEOF\n")
else -- display_server == 'xorg'
os.execute("xclip -selection clipboard <<EOF\n" .. text .. "\nEOF\n")
end
end
local function macos_set_clipboard(text)
os.execute("export LANG=en_US.UTF-8; cat <<EOF | pbcopy\n" .. text .. "\nEOF\n")
end
local function record_sub(_, text)
if text and mp.get_property_number("sub-start") and mp.get_property_number("sub-end") then
local sub_delay = mp.get_property_native("sub-delay")
local audio_delay = mp.get_property_native("audio-delay")
local newtext = clean(text)
if newtext == "" then
return
end
subs[newtext] = {
mp.get_property_number("sub-start") + sub_delay - audio_delay,
mp.get_property_number("sub-end") + sub_delay - audio_delay,
}
dlog(string.format("%s -> %s : %s", subs[newtext][1], subs[newtext][2], newtext))
if ENABLE_SUBS_TO_CLIP then
-- Remove newlines from text before sending it to clipboard.
-- This way pressing control+v without copying from texthooker page
-- will always give last line.
text = string.gsub(text, "[\n\r]+", " ")
if platform == "windows" then
if use_powershell_clipboard == nil then
determine_clip_type()
end
if use_powershell_clipboard then
powershell_set_clipboard(text)
else
cmd_set_clipboard(text)
end
elseif platform == "macos" then
macos_set_clipboard(text)
else
linux_set_clipboard(text)
end
end
end
end
local function create_audio(s, e)
if s == nil or e == nil then
return
end
local name = get_name(s, e)
local destination = utils.join_path(prefix, name .. ".mp3")
s = s - AUDIO_CLIP_PADDING
local t = e - s + AUDIO_CLIP_PADDING
local source = mp.get_property("path")
local aid = mp.get_property("aid")
local tracks_count = mp.get_property_number("track-list/count")
for i = 1, tracks_count do
local track_type = mp.get_property(string.format("track-list/%d/type", i))
local track_selected = mp.get_property(string.format("track-list/%d/selected", i))
if track_type == "audio" and track_selected == "yes" then
if mp.get_property(string.format("track-list/%d/external-filename", i), o) ~= o then
source = mp.get_property(string.format("track-list/%d/external-filename", i))
aid = "auto"
end
break
end
end
local cmd = {
"run",
"mpv",
source,
"--loop-file=no",
"--video=no",
"--no-ocopy-metadata",
"--no-sub",
"--audio-channels=1",
string.format("--start=%.3f", s),
string.format("--length=%.3f", t),
string.format("--aid=%s", aid),
string.format("--volume=%s", USE_MPV_VOLUME and mp.get_property("volume") or "100"),
string.format("--af-append=afade=t=in:curve=ipar:st=%.3f:d=%.3f", s, AUDIO_CLIP_FADE),
string.format("--af-append=afade=t=out:curve=ipar:st=%.3f:d=%.3f", s + t - AUDIO_CLIP_FADE, AUDIO_CLIP_FADE),
string.format("-o=%s", destination),
}
mp.commandv(table.unpack(cmd))
dlog(utils.to_string(cmd))
end
local function create_screenshot(s, e)
local source = mp.get_property("path")
local img = utils.join_path(prefix, get_name(s, e) .. "." .. IMAGE_FORMAT)
local cmd = {
"run",
"mpv",
source,
"--loop-file=no",
"--audio=no",
"--no-ocopy-metadata",
"--no-sub",
"--frames=1",
}
if IMAGE_FORMAT == "webp" then
table.insert(cmd, "--ovc=libwebp")
table.insert(cmd, "--ovcopts-add=lossless=0")
table.insert(cmd, "--ovcopts-add=compression_level=6")
table.insert(cmd, "--ovcopts-add=preset=drawing")
elseif IMAGE_FORMAT == "png" then
table.insert(cmd, "--vf-add=format=rgb24")
end
table.insert(cmd, "--vf-add=scale=480*iw*sar/ih:480")
table.insert(cmd, string.format("--start=%.3f", mp.get_property_number("time-pos")))
table.insert(cmd, string.format("-o=%s", img))
mp.commandv(table.unpack(cmd))
dlog(utils.to_string(cmd))
end
local function add_to_last_added(ifield, afield, tfield)
local added_notes = anki_connect("findNotes", { query = "added:1" })["result"]
table.sort(added_notes)
local noteid = added_notes[#added_notes]
local note = anki_connect("notesInfo", { notes = { noteid } })
if note ~= nil then
local word = note["result"][1]["fields"][FRONT_FIELD]["value"]
local new_fields = {
[SENTENCE_AUDIO_FIELD] = afield,
[SENTENCE_FIELD] = tfield,
[IMAGE_FIELD] = ifield,
}
anki_connect("updateNoteFields", {
note = {
id = noteid,
fields = new_fields,
},
})
mp.osd_message("Updated note: " .. word, 3)
msg.info("Updated note: " .. word)
end
end
local function get_extract()
local lines = get_clipboard()
local e = 0
local s = 0
for line in lines:gmatch("[^\r\n]+") do
line = clean(line)
dlog(line)
if subs[line] ~= nil then
if subs[line][1] ~= nil and subs[line][2] ~= nil then
if s == 0 then
s = subs[line][1]
else
s = math.min(s, subs[line][1])
end
e = math.max(e, subs[line][2])
end
else
mp.osd_message("ERR! Line not found: " .. line, 3)
return
end
end
dlog(string.format("s=%d, e=%d", s, e))
if e ~= 0 then
create_screenshot(s, e)
create_audio(s, e)
local ifield = "<img src=" .. get_name(s, e) .. "." .. IMAGE_FORMAT .. ">"
local afield = "[sound:" .. get_name(s, e) .. ".mp3]"
local tfield = string.gsub(string.gsub(lines, "\n+", "<br />"), "\r", "")
add_to_last_added(ifield, afield, tfield)
if AUTOPLAY_AUDIO then
local name = get_name(s, e)
local audio = utils.join_path(prefix, name .. ".mp3")
local cmd = { "run", "mpv", audio, "--loop-file=no", "--load-scripts=no" }
mp.commandv(table.unpack(cmd))
end
end
end
local function ex()
if not prefix or prefix == "" then
set_media_dir()
end
if debug_mode then
get_extract()
else
pcall(get_extract)
end
end
local function rec(...)
if debug_mode then
record_sub(...)
else
pcall(record_sub, ...)
end
end
local function toggle_sub_to_clipboard()
ENABLE_SUBS_TO_CLIP = not ENABLE_SUBS_TO_CLIP
mp.osd_message("Clipboard inserter " .. (ENABLE_SUBS_TO_CLIP and "activated" or "deactived"), 3)
end
local function toggle_debug_mode()
debug_mode = not debug_mode
mp.osd_message("Debug mode " .. (debug_mode and "activated" or "deactived"), 3)
end
local function clear_subs(_)
subs = {}
end
mp.observe_property("sub-text", "string", rec)
mp.observe_property("filename", "string", clear_subs)
mp.add_key_binding("ctrl+v", "update-anki-card", ex)
mp.add_key_binding("ctrl+t", "toggle-clipboard-insertion", toggle_sub_to_clipboard)
mp.add_key_binding("ctrl+d", "toggle-debug-mode", toggle_debug_mode)
mp.add_key_binding("ctrl+V", ex)
mp.add_key_binding("ctrl+T", toggle_sub_to_clipboard)
mp.add_key_binding("ctrl+D", toggle_debug_mode)

1
scripts/autosubsync-mpv Symbolic link
View File

@@ -0,0 +1 @@
../submodules/autosubsync-mpv

1
scripts/immersion-tracker Symbolic link
View File

@@ -0,0 +1 @@
../submodules/immersion-tracker

388
scripts/jimaku.js Normal file
View File

@@ -0,0 +1,388 @@
// Go to https://jimaku.cc/login and create a new account.
// Then go to https://jimaku.cc/account and click the `Generate` button to create a new API key
// Click the `Copy` button and paste it below
var API_KEY = "";
// Configuration options
var CONFIG = {
// Filter the response to only have the specified episode
prompt_episode: true,
// Subtitle suffix (e.g., ".JA" for Japanese subtitles)
subtitle_suffix: ".JA",
// Preferred subtitle format (order matters, first is most preferred)
preferred_formats: ["ass", "srt", "vtt"],
// Automatically load the subtitle after download
auto_load: true,
// Default subtitle delay in seconds (can be positive or negative)
default_delay: 0,
// Default subtitle font size
default_font_size: 16,
// Automatically rename the subtitle file after download
auto_rename: true,
// Automatically run autosubsync-mpv after downloading the subtitle
run_auto_subsync: true
};
// Keybindings
// var MANUAL_SEARCH_KEY = "g";
var FILENAME_AUTO_SEARCH_KEY = "ctrl+J";
var PARENT_FOLDER_AUTO_SEARCH_KEY = "n";
function api(url, extraArgs) {
var baseArgs = [
"curl",
"-s",
"--url",
url,
"--header",
"Authorization: " + API_KEY
];
var args = Array.prototype.concat.apply(baseArgs, extraArgs);
var res = mp.command_native({
name: "subprocess",
playback_only: false,
capture_stdout: true,
capture_stderr: true,
args: args
});
if (res.stdout) return JSON.parse(res.stdout);
}
function downloadSub(sub) {
return api(sub.url, ["--output", sub.name]);
}
function showMessage(message, persist) {
var ass_start = mp.get_property_osd("osd-ass-cc/0");
var ass_stop = mp.get_property_osd("osd-ass-cc/1");
mp.osd_message(
ass_start + "{\\fs16}" + message + ass_stop,
persist ? 999 : 2
);
}
// The timeout is neccessary due to a weird bug in mpv
function inputGet(args) {
mp.input.terminate();
setTimeout(function () {
mp.input.get(args);
}, 1);
}
// The timeout is neccessary due to a weird bug in mpv
function inputSelect(args) {
mp.input.terminate();
setTimeout(function () {
mp.input.select(args);
}, 1);
}
// Taken from mpv-subversive
// https://github.com/nairyosangha/mpv-subversive/blob/master/backend/backend.lua#L146
function sanitize(text) {
var subPatterns = [
/\.[a-zA-Z]+$/, // extension
/\./g,
/-/g,
/_/g,
/\[[^\]]+\]/g, // [] bracket
/\([^\)]+\)/g, // () bracket
/720[pP]/g,
/480[pP]/g,
/1080[pP]/g,
/[xX]26[45]/g,
/[bB]lu[-]?[rR]ay/g,
/^[\s]*/,
/[\s]*$/,
/1920x1080/g,
/1920X1080/g,
/Hi10P/g,
/FLAC/g,
/AAC/g
];
var result = text;
subPatterns.forEach(function (subPattern) {
var newResult = result.replace(subPattern, " ");
if (newResult.length > 0) {
result = newResult;
}
});
return result;
}
// Adapted from mpv-subversive
// https://github.com/nairyosangha/mpv-subversive/blob/master/backend/backend.lua#L164
function extractTitle(text) {
var matchers = [
{ regex: /^([\w\s\d]+)[Ss]\d+[Ee]?\d+/, group: 1 },
{ regex: /^([\w\s\d]+)-[\s]*\d+[\s]*[^\w]*$/, group: 1 },
{ regex: /^([\w\s\d]+)[Ee]?[Pp]?[\s]+\d+$/, group: 1 },
{ regex: /^([\w\s\d]+)[\s]\d+.*$/, group: 1 },
{ regex: /^\d+[\s]*(.+)$/, group: 1 }
];
for (var i = 0; i < matchers.length; i++) {
var matcher = matchers[i];
var match = text.match(matcher.regex);
if (match) {
return match[matcher.group].trim();
}
}
return text;
}
function getNames(results) {
return results.map(function (item) {
return item.name;
});
}
function runAutoSubSyncMPV() {
try {
mp.command_native(["script-binding", "autosubsync-menu"]);
} catch (e) {
showMessage("autosubsync-mpv not installed");
return;
}
}
function selectSub(selectedSub) {
showMessage("Downloading: " + selectedSub.name);
try {
downloadSub(selectedSub);
// Get current video filename without extension
var videoPath = mp.get_property("path");
if (!videoPath) {
throw new Error("No video file is currently playing");
}
var videoName = videoPath.substring(0, videoPath.lastIndexOf("."));
// Get subtitle extension
var subExt = selectedSub.name.substring(selectedSub.name.lastIndexOf("."));
var newSubName = selectedSub.name;
if (CONFIG.auto_rename) {
// Create new subtitle filename
newSubName = videoName + CONFIG.subtitle_suffix + subExt;
// Rename the downloaded subtitle file
var renameResult = mp.command_native({
name: "subprocess",
playback_only: false,
args: ["mv", selectedSub.name, newSubName]
});
if (renameResult.error) {
throw new Error(
"Failed to rename subtitle file: " + renameResult.error
);
}
showMessage(newSubName + " downloaded and renamed");
} else {
showMessage(newSubName + " downloaded");
}
if (CONFIG.auto_load) {
mp.commandv("sub_add", newSubName);
showMessage(newSubName + " added");
// Apply subtitle settings if configured
if (CONFIG.default_delay !== 0) {
mp.commandv("sub_delay", CONFIG.default_delay);
}
if (CONFIG.default_font_size !== 16) {
mp.commandv("sub_font_size", CONFIG.default_font_size);
}
}
if (CONFIG.run_auto_subsync) {
runAutoSubSyncMPV();
}
mp.set_property("pause", "no");
} catch (error) {
showMessage("Error: " + error.message, true);
mp.set_property("pause", "no");
}
}
function sortByPreferredFormat(files) {
return files.sort(function (a, b) {
var extA = a.name.substring(a.name.lastIndexOf(".") + 1).toLowerCase();
var extB = b.name.substring(b.name.lastIndexOf(".") + 1).toLowerCase();
var indexA = CONFIG.preferred_formats.indexOf(extA);
var indexB = CONFIG.preferred_formats.indexOf(extB);
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
});
}
function selectEpisode(anime, episode) {
mp.input.terminate();
var episodeResults;
if (episode) {
showMessage("Fetching subs for: " + anime.name + " episode " + episode);
episodeResults = api(
"https://jimaku.cc/api/entries/" + anime.id + "/files?episode=" + episode
);
} else {
showMessage("Fetching all subs for: " + anime.name);
episodeResults = api(
"https://jimaku.cc/api/entries/" + anime.id + "/files"
);
}
if (episodeResults.error) {
showMessage("Error: " + animeResults.error);
return;
}
if (episodeResults.length === 0) {
showMessage("No results found");
return;
}
// Sort results by preferred format
episodeResults = sortByPreferredFormat(episodeResults);
if (episodeResults.length === 1) {
var selectedEpisode = episodeResults[0];
selectSub(selectedEpisode);
return;
}
var items = getNames(episodeResults);
inputSelect({
prompt: "Select episode: ",
items: items,
submit: function (id) {
var selectedEpisode = episodeResults[id - 1];
selectSub(selectedEpisode);
}
});
}
function onAnimeSelected(anime) {
if (CONFIG.prompt_episode) {
inputGet({
prompt: "Episode (leave blank for all): ",
submit: function (episode) {
selectEpisode(anime, episode);
}
});
} else {
selectEpisode(anime);
}
}
function search(searchTerm, isAuto) {
mp.input.terminate();
showMessage('Searching for: "' + searchTerm + '"');
var animeResults = api(
encodeURI(
"https://jimaku.cc/api/entries/search?anime=true&query=" + searchTerm
)
);
if (animeResults.error) {
showMessage("Error: " + animeResults.error);
return;
}
if (animeResults.length === 0) {
showMessage("No results found");
if (isAuto) {
manualSearch(searchTerm);
}
return;
}
if (animeResults.length === 1) {
var selectedAnime = animeResults[0];
onAnimeSelected(selectedAnime);
return;
}
var items = getNames(animeResults);
inputSelect({
prompt: "Select anime: ",
items: items,
submit: function (id) {
var selectedAnime = animeResults[id - 1];
showMessage(selectedAnime.name, true);
onAnimeSelected(selectedAnime);
}
});
}
function manualSearch(defaultText) {
inputGet({
prompt: "Search term: ",
submit: search,
default_text: defaultText
});
mp.set_property("pause", "yes");
showMessage("Manual Jimaku Search", true);
}
function autoSearch() {
var filename = mp.get_property("filename");
var sanitizedFilename = sanitize(filename);
var currentAnime = extractTitle(sanitizedFilename);
mp.set_property("pause", "yes");
search(currentAnime, true);
}
function autoSearchParentFolder() {
var path = mp.get_property("stream-open-filename");
var pathSplit = path.split(path.indexOf("/") >= 0 ? "/" : "\\");
var filename =
pathSplit.length === 1 ? pathSplit[0] : pathSplit[pathSplit.length - 2];
var sanitizedFilename = sanitize(filename);
var currentAnime = extractTitle(sanitizedFilename);
mp.set_property("pause", "yes");
search(currentAnime, true);
}
// mp.add_key_binding(MANUAL_SEARCH_KEY, "jimaku-manual-search", manualSearch);
mp.add_key_binding(
FILENAME_AUTO_SEARCH_KEY,
"jimaku-filename-auto-search",
autoSearch
);
mp.add_key_binding(
PARENT_FOLDER_AUTO_SEARCH_KEY,
"jimaku-parent-folder-auto-search",
autoSearchParentFolder
);

View File

@@ -1 +1 @@
../ModernZ/modernz.lua ../submodules/ModernZ/modernz.lua

View File

@@ -1 +1 @@
../mpv-youtube-queue/mpv-youtube-queue.lua ../submodules/mpv-youtube-queue/mpv-youtube-queue.lua

View File

@@ -8,75 +8,75 @@ local platform = mp.get_property_native("platform")
local config_file_path = mp.find_config_file("mpv.conf") local config_file_path = mp.find_config_file("mpv.conf")
local config_folder_path, config_file = utils.split_path(config_file_path) local config_folder_path, config_file = utils.split_path(config_file_path)
local mpv_websocket_path = local mpv_websocket_path =
utils.join_path(config_folder_path, platform == "windows" and "mpv_websocket.exe" or "mpv_websocket") utils.join_path(config_folder_path, platform == "windows" and "mpv_websocket.exe" or "mpv_websocket")
local initialised_websocket local initialised_websocket
local _, err = utils.file_info(config_file_path) local _, err = utils.file_info(config_file_path)
if err then if err then
error("failed to open mpv config file `" .. config_file_path .. "`") error("failed to open mpv config file `" .. config_file_path .. "`")
end end
local _, err = utils.file_info(mpv_websocket_path) local _, err = utils.file_info(mpv_websocket_path)
if err then if err then
error("failed to open mpv_websocket") error("failed to open mpv_websocket")
end end
local function find_mpv_socket(config_file_path) local function find_mpv_socket(config_file_path)
local file = io.open(config_file_path, "r") local file = io.open(config_file_path, "r")
if file == nil then if file == nil then
error("failed to read mpv config file `" .. config_file_path .. "`") error("failed to read mpv config file `" .. config_file_path .. "`")
end end
local mpv_socket local mpv_socket
for line in file:lines() do for line in file:lines() do
mpv_socket = line:match("^input%-ipc%-server%s*=%s*(%g+)%s*") mpv_socket = line:match("^input%-ipc%-server%s*=%s*(%g+)%s*")
if mpv_socket then if mpv_socket then
break break
end end
end end
file:close() file:close()
if not mpv_socket then if not mpv_socket then
error("input-ipc-server option does not exist in `" .. config_file_path .. "`") error("input-ipc-server option does not exist in `" .. config_file_path .. "`")
end end
return mpv_socket return mpv_socket
end end
local mpv_socket = find_mpv_socket(config_file_path) local mpv_socket = find_mpv_socket(config_file_path)
if platform == "windows" then if platform == "windows" then
mpv_socket = "\\\\.\\pipe" .. mpv_socket:gsub("/", "\\") mpv_socket = "\\\\.\\pipe" .. mpv_socket:gsub("/", "\\")
end end
local function start_websocket() local function start_websocket()
initialised_websocket = mp.command_native_async({ initialised_websocket = mp.command_native_async({
name = "subprocess", name = "subprocess",
playback_only = false, playback_only = false,
capture_stdout = true, capture_stdout = true,
capture_stderr = true, capture_stderr = true,
args = { args = {
mpv_websocket_path, mpv_websocket_path,
"-m", "-m",
mpv_socket, mpv_socket,
"-w", "-w",
"6677", "6677",
}, },
}) })
end end
local function end_websocket() local function end_websocket()
mp.abort_async_command(initialised_websocket) mp.abort_async_command(initialised_websocket)
initialised_websocket = nil initialised_websocket = nil
end end
local function toggle_websocket() local function toggle_websocket()
local paused = mp.get_property_bool("pause") local paused = mp.get_property_bool("pause")
if initialised_websocket and paused then if initialised_websocket and paused then
end_websocket() end_websocket()
elseif not initialised_websocket and not paused then elseif not initialised_websocket and not paused then
start_websocket() start_websocket()
end end
end end
mp.register_script_message("togglewebsocket", toggle_websocket) mp.register_script_message("togglewebsocket", toggle_websocket)

1
scripts/subs2srs Symbolic link
View File

@@ -0,0 +1 @@
../submodules/mpvacious

View File

@@ -1,921 +0,0 @@
-- thumbfast.lua
--
-- High-performance on-the-fly thumbnailer
--
-- Built for easy integration in third-party UIs.
local options = {
-- Socket path (leave empty for auto)
socket = "",
-- Thumbnail path (leave empty for auto)
thumbnail = "",
-- Maximum thumbnail size in pixels (scaled down to fit)
-- Values are scaled when hidpi is enabled
max_height = 200,
max_width = 200,
-- Apply tone-mapping, no to disable
tone_mapping = "auto",
-- Overlay id
overlay_id = 42,
-- Spawn thumbnailer on file load for faster initial thumbnails
spawn_first = false,
-- Close thumbnailer process after an inactivity period in seconds, 0 to disable
quit_after_inactivity = 0,
-- Enable on network playback
network = false,
-- Enable on audio playback
audio = false,
-- Enable hardware decoding
hwdec = false,
-- Windows only: use native Windows API to write to pipe (requires LuaJIT)
direct_io = false,
-- Custom path to the mpv executable
mpv_path = "mpv"
}
mp.utils = require "mp.utils"
mp.options = require "mp.options"
mp.options.read_options(options, "thumbfast")
local properties = {}
local pre_0_30_0 = mp.command_native_async == nil
local pre_0_33_0 = true
function subprocess(args, async, callback)
callback = callback or function() end
if not pre_0_30_0 then
if async then
return mp.command_native_async({name = "subprocess", playback_only = true, args = args}, callback)
else
return mp.command_native({name = "subprocess", playback_only = false, capture_stdout = true, args = args})
end
else
if async then
return mp.utils.subprocess_detached({args = args}, callback)
else
return mp.utils.subprocess({args = args})
end
end
end
local winapi = {}
if options.direct_io then
local ffi_loaded, ffi = pcall(require, "ffi")
if ffi_loaded then
winapi = {
ffi = ffi,
C = ffi.C,
bit = require("bit"),
socket_wc = "",
-- WinAPI constants
CP_UTF8 = 65001,
GENERIC_WRITE = 0x40000000,
OPEN_EXISTING = 3,
FILE_FLAG_WRITE_THROUGH = 0x80000000,
FILE_FLAG_NO_BUFFERING = 0x20000000,
PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001),
INVALID_HANDLE_VALUE = ffi.cast("void*", -1),
-- don't care about how many bytes WriteFile wrote, so allocate something to store the result once
_lpNumberOfBytesWritten = ffi.new("unsigned long[1]"),
}
-- cache flags used in run() to avoid bor() call
winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING)
ffi.cdef[[
void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile);
bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped);
bool __stdcall CloseHandle(void *hObject);
bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout);
int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
]]
winapi.MultiByteToWideChar = function(MultiByteStr)
if MultiByteStr then
local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, nil, 0)
if utf16_len > 0 then
local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len)
if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then
return utf16_str
end
end
end
return ""
end
else
options.direct_io = false
end
end
local file = nil
local file_bytes = 0
local spawned = false
local disabled = false
local force_disabled = false
local spawn_waiting = false
local spawn_working = false
local script_written = false
local dirty = false
local x = nil
local y = nil
local last_x = x
local last_y = y
local last_seek_time = nil
local effective_w = options.max_width
local effective_h = options.max_height
local real_w = nil
local real_h = nil
local last_real_w = nil
local last_real_h = nil
local script_name = nil
local show_thumbnail = false
local filters_reset = {["lavfi-crop"]=true, ["crop"]=true}
local filters_runtime = {["hflip"]=true, ["vflip"]=true}
local filters_all = {["hflip"]=true, ["vflip"]=true, ["lavfi-crop"]=true, ["crop"]=true}
local tone_mappings = {["none"]=true, ["clip"]=true, ["linear"]=true, ["gamma"]=true, ["reinhard"]=true, ["hable"]=true, ["mobius"]=true}
local last_tone_mapping = nil
local last_vf_reset = ""
local last_vf_runtime = ""
local last_rotate = 0
local par = ""
local last_par = ""
local last_has_vid = 0
local has_vid = 0
local file_timer = nil
local file_check_period = 1/60
local allow_fast_seek = true
local client_script = [=[
#!/usr/bin/env bash
MPV_IPC_FD=0; MPV_IPC_PATH="%s"
trap "kill 0" EXIT
while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done
if echo "print-text thumbfast" >&"$MPV_IPC_FD"; then echo -n > "$MPV_IPC_PATH"; tail -f "$MPV_IPC_PATH" >&"$MPV_IPC_FD" & while read -r -u "$MPV_IPC_FD" 2>/dev/null; do :; done; fi
]=]
local function get_os()
local raw_os_name = ""
if jit and jit.os and jit.arch then
raw_os_name = jit.os
else
if package.config:sub(1,1) == "\\" then
-- Windows
local env_OS = os.getenv("OS")
if env_OS then
raw_os_name = env_OS
end
else
raw_os_name = subprocess({"uname", "-s"}).stdout
end
end
raw_os_name = (raw_os_name):lower()
local os_patterns = {
["windows"] = "windows",
["linux"] = "linux",
["osx"] = "darwin",
["mac"] = "darwin",
["darwin"] = "darwin",
["^mingw"] = "windows",
["^cygwin"] = "windows",
["bsd$"] = "darwin",
["sunos"] = "darwin"
}
-- Default to linux
local str_os_name = "linux"
for pattern, name in pairs(os_patterns) do
if raw_os_name:match(pattern) then
str_os_name = name
break
end
end
return str_os_name
end
local os_name = mp.get_property("platform") or get_os()
local path_separator = os_name == "windows" and "\\" or "/"
if options.socket == "" then
if os_name == "windows" then
options.socket = "thumbfast"
else
options.socket = "/tmp/thumbfast"
end
end
if options.thumbnail == "" then
if os_name == "windows" then
options.thumbnail = os.getenv("TEMP").."\\thumbfast.out"
else
options.thumbnail = "/tmp/thumbfast.out"
end
end
local unique = mp.utils.getpid()
options.socket = options.socket .. unique
options.thumbnail = options.thumbnail .. unique
if options.direct_io then
if os_name == "windows" then
winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket)
end
if winapi.socket_wc == "" then
options.direct_io = false
end
end
local mpv_path = options.mpv_path
if mpv_path == "mpv" and os_name == "darwin" and unique then
-- TODO: look into ~~osxbundle/
mpv_path = string.gsub(subprocess({"ps", "-o", "comm=", "-p", tostring(unique)}).stdout, "[\n\r]", "")
if mpv_path ~= "mpv" then
mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv")
local mpv_bin = mp.utils.file_info("/usr/local/mpv")
if mpv_bin and mpv_bin.is_file then
mpv_path = "/usr/local/mpv"
else
local mpv_app = mp.utils.file_info("/Applications/mpv.app/Contents/MacOS/mpv")
if mpv_app and mpv_app.is_file then
mp.msg.warn("symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
else
mp.msg.warn("drag to your Applications folder and symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`")
end
end
end
end
local function vo_tone_mapping()
local passes = mp.get_property_native("vo-passes")
if passes and passes["fresh"] then
for k, v in pairs(passes["fresh"]) do
for k2, v2 in pairs(v) do
if k2 == "desc" and v2 then
local tone_mapping = string.match(v2, "([0-9a-z.-]+) tone map")
if tone_mapping then
return tone_mapping
end
end
end
end
end
end
local function vf_string(filters, full)
local vf = ""
local vf_table = properties["vf"]
if vf_table and #vf_table > 0 then
for i = #vf_table, 1, -1 do
if filters[vf_table[i].name] then
local args = ""
for key, value in pairs(vf_table[i].params) do
if args ~= "" then
args = args .. ":"
end
args = args .. key .. "=" .. value
end
vf = vf .. vf_table[i].name .. "=" .. args .. ","
end
end
end
if (full and options.tone_mapping ~= "no") or options.tone_mapping == "auto" then
if properties["video-params"] and properties["video-params"]["primaries"] == "bt.2020" then
local tone_mapping = options.tone_mapping
if tone_mapping == "auto" then
tone_mapping = last_tone_mapping or properties["tone-mapping"]
if tone_mapping == "auto" and properties["current-vo"] == "gpu-next" then
tone_mapping = vo_tone_mapping()
end
end
if not tone_mappings[tone_mapping] then
tone_mapping = "hable"
end
last_tone_mapping = tone_mapping
vf = vf .. "zscale=transfer=linear,format=gbrpf32le,tonemap="..tone_mapping..",zscale=transfer=bt709,"
end
end
if full then
vf = vf.."scale=w="..effective_w..":h="..effective_h..par..",pad=w="..effective_w..":h="..effective_h..":x=-1:y=-1,format=bgra"
end
return vf
end
local function calc_dimensions()
local width = properties["video-out-params"] and properties["video-out-params"]["dw"]
local height = properties["video-out-params"] and properties["video-out-params"]["dh"]
if not width or not height then return end
local scale = properties["display-hidpi-scale"] or 1
if width / height > options.max_width / options.max_height then
effective_w = math.floor(options.max_width * scale + 0.5)
effective_h = math.floor(height / width * effective_w + 0.5)
else
effective_h = math.floor(options.max_height * scale + 0.5)
effective_w = math.floor(width / height * effective_h + 0.5)
end
local v_par = properties["video-out-params"] and properties["video-out-params"]["par"] or 1
if v_par == 1 then
par = ":force_original_aspect_ratio=decrease"
else
par = ""
end
end
local info_timer = nil
local function info(w, h)
local rotate = properties["video-params"] and properties["video-params"]["rotate"]
local image = properties["current-tracks"] and properties["current-tracks"]["video"] and properties["current-tracks"]["video"]["image"]
local albumart = image and properties["current-tracks"]["video"]["albumart"]
disabled = (w or 0) == 0 or (h or 0) == 0 or
has_vid == 0 or
(properties["demuxer-via-network"] and not options.network) or
(albumart and not options.audio) or
(image and not albumart) or
force_disabled
if info_timer then
info_timer:kill()
info_timer = nil
elseif has_vid == 0 or (rotate == nil and not disabled) then
info_timer = mp.add_timeout(0.05, function() info(w, h) end)
end
local json, err = mp.utils.format_json({width=w, height=h, disabled=disabled, available=true, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
if pre_0_30_0 then
mp.command_native({"script-message", "thumbfast-info", json})
else
mp.command_native_async({"script-message", "thumbfast-info", json}, function() end)
end
end
local function remove_thumbnail_files()
if file then
file:close()
file = nil
file_bytes = 0
end
os.remove(options.thumbnail)
os.remove(options.thumbnail..".bgra")
end
local activity_timer
local function spawn(time)
if disabled then return end
local path = properties["path"]
if path == nil then return end
if options.quit_after_inactivity > 0 then
if show_thumbnail or activity_timer:is_enabled() then
activity_timer:kill()
end
activity_timer:resume()
end
local open_filename = properties["stream-open-filename"]
local ytdl = open_filename and properties["demuxer-via-network"] and path ~= open_filename
if ytdl then
path = open_filename
end
remove_thumbnail_files()
local vid = properties["vid"]
has_vid = vid or 0
local args = {
mpv_path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always", "--really-quiet", "--no-terminal",
"--load-scripts=no", "--osc=no", "--ytdl=no", "--load-stats-overlay=no", "--load-osd-console=no", "--load-auto-profiles=no",
"--edition="..(properties["edition"] or "auto"), "--vid="..(vid or "auto"), "--no-sub", "--no-audio",
"--start="..time, allow_fast_seek and "--hr-seek=no" or "--hr-seek=yes",
"--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB",
"--vd-lavc-skiploopfilter=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast", "--vd-lavc-threads=2", "--hwdec="..(options.hwdec and "auto" or "no"),
"--vf="..vf_string(filters_all, true),
"--sws-scaler=fast-bilinear",
"--video-rotate="..last_rotate,
"--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o="..options.thumbnail
}
if not pre_0_30_0 then
table.insert(args, "--sws-allow-zimg=no")
end
if os_name == "darwin" and properties["macos-app-activation-policy"] then
table.insert(args, "--macos-app-activation-policy=accessory")
end
if os_name == "windows" or pre_0_33_0 then
table.insert(args, "--input-ipc-server="..options.socket)
elseif not script_written then
local client_script_path = options.socket..".run"
local script = io.open(client_script_path, "w+")
if script == nil then
mp.msg.error("client script write failed")
return
else
script_written = true
script:write(string.format(client_script, options.socket))
script:close()
subprocess({"chmod", "+x", client_script_path}, true)
table.insert(args, "--scripts="..client_script_path)
end
else
local client_script_path = options.socket..".run"
table.insert(args, "--scripts="..client_script_path)
end
table.insert(args, "--")
table.insert(args, path)
spawned = true
spawn_waiting = true
subprocess(args, true,
function(success, result)
if spawn_waiting and (success == false or (result.status ~= 0 and result.status ~= -2)) then
spawned = false
spawn_waiting = false
options.tone_mapping = "no"
mp.msg.error("mpv subprocess create failed")
if not spawn_working then -- notify users of required configuration
if options.mpv_path == "mpv" then
if properties["current-vo"] == "libmpv" then
if options.mpv_path == mpv_path then -- attempt to locate ImPlay
mpv_path = "ImPlay"
spawn(time)
else -- ImPlay not in path
if os_name ~= "darwin" then
force_disabled = true
info(real_w or effective_w, real_h or effective_h)
end
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
end
else
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
if os_name == "windows" then
mp.commandv("script-message-to", "mpvnet", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
mp.commandv("script-message", "mpv.net", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20)
end
end
else
mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000)
-- found ImPlay but not defined in config
mp.commandv("script-message-to", "implay", "show-message", "thumbfast", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
end
end
elseif success == true and (result.status == 0 or result.status == -2) then
if not spawn_working and properties["current-vo"] == "libmpv" and options.mpv_path ~= mpv_path then
mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay")
end
spawn_working = true
spawn_waiting = false
end
end
)
end
local function run(command)
if not spawned then return end
if options.direct_io then
local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, winapi._createfile_pipe_flags, nil)
if hPipe ~= winapi.INVALID_HANDLE_VALUE then
local buf = command .. "\n"
winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil)
winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil)
winapi.C.CloseHandle(hPipe)
end
return
end
local command_n = command.."\n"
if os_name == "windows" then
if file and file_bytes + #command_n >= 4096 then
file:close()
file = nil
file_bytes = 0
end
if not file then
file = io.open("\\\\.\\pipe\\"..options.socket, "r+b")
end
elseif pre_0_33_0 then
subprocess({"/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket})
return
elseif not file then
file = io.open(options.socket, "r+")
end
if file then
file_bytes = file:seek("end")
file:write(command_n)
file:flush()
end
end
local function draw(w, h, script)
if not w or not show_thumbnail then return end
if x ~= nil then
if pre_0_30_0 then
mp.command_native({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w)})
else
mp.command_native_async({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w)}, function() end)
end
elseif script then
local json, err = mp.utils.format_json({width=w, height=h, x=x, y=y, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
mp.commandv("script-message-to", script, "thumbfast-render", json)
end
end
local function real_res(req_w, req_h, filesize)
local count = filesize / 4
local diff = (req_w * req_h) - count
if (properties["video-params"] and properties["video-params"]["rotate"] or 0) % 180 == 90 then
req_w, req_h = req_h, req_w
end
if diff == 0 then
return req_w, req_h
else
local threshold = 5 -- throw out results that change too much
local long_side, short_side = req_w, req_h
if req_h > req_w then
long_side, short_side = req_h, req_w
end
for a = short_side, short_side - threshold, -1 do
if count % a == 0 then
local b = count / a
if long_side - b < threshold then
if req_h < req_w then return b, a else return a, b end
end
end
end
return nil
end
end
local function move_file(from, to)
if os_name == "windows" then
os.remove(to)
end
-- move the file because it can get overwritten while overlay-add is reading it, and crash the player
os.rename(from, to)
end
local function seek(fast)
if last_seek_time then
run("async seek " .. last_seek_time .. (fast and " absolute+keyframes" or " absolute+exact"))
end
end
local seek_period = 3/60
local seek_period_counter = 0
local seek_timer
seek_timer = mp.add_periodic_timer(seek_period, function()
if seek_period_counter == 0 then
seek(allow_fast_seek)
seek_period_counter = 1
else
if seek_period_counter == 2 then
if allow_fast_seek then
seek_timer:kill()
seek()
end
else seek_period_counter = seek_period_counter + 1 end
end
end)
seek_timer:kill()
local function request_seek()
if seek_timer:is_enabled() then
seek_period_counter = 0
else
seek_timer:resume()
seek(allow_fast_seek)
seek_period_counter = 1
end
end
local function check_new_thumb()
-- the slave might start writing to the file after checking existance and
-- validity but before actually moving the file, so move to a temporary
-- location before validity check to make sure everything stays consistant
-- and valid thumbnails don't get overwritten by invalid ones
local tmp = options.thumbnail..".tmp"
move_file(options.thumbnail, tmp)
local finfo = mp.utils.file_info(tmp)
if not finfo then return false end
spawn_waiting = false
local w, h = real_res(effective_w, effective_h, finfo.size)
if w then -- only accept valid thumbnails
move_file(tmp, options.thumbnail..".bgra")
real_w, real_h = w, h
if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then
last_real_w, last_real_h = real_w, real_h
info(real_w, real_h)
end
if not show_thumbnail then
file_timer:kill()
end
return true
end
return false
end
file_timer = mp.add_periodic_timer(file_check_period, function()
if check_new_thumb() then
draw(real_w, real_h, script_name)
end
end)
file_timer:kill()
local function clear()
file_timer:kill()
seek_timer:kill()
if options.quit_after_inactivity > 0 then
if show_thumbnail or activity_timer:is_enabled() then
activity_timer:kill()
end
activity_timer:resume()
end
last_seek_time = nil
show_thumbnail = false
last_x = nil
last_y = nil
if script_name then return end
if pre_0_30_0 then
mp.command_native({"overlay-remove", options.overlay_id})
else
mp.command_native_async({"overlay-remove", options.overlay_id}, function() end)
end
end
local function quit()
activity_timer:kill()
if show_thumbnail then
activity_timer:resume()
return
end
run("quit")
spawned = false
real_w, real_h = nil, nil
clear()
end
activity_timer = mp.add_timeout(options.quit_after_inactivity, quit)
activity_timer:kill()
local function thumb(time, r_x, r_y, script)
if disabled then return end
time = tonumber(time)
if time == nil then return end
if r_x == "" or r_y == "" then
x, y = nil, nil
else
x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5)
end
script_name = script
if last_x ~= x or last_y ~= y or not show_thumbnail then
show_thumbnail = true
last_x = x
last_y = y
draw(real_w, real_h, script)
end
if options.quit_after_inactivity > 0 then
if show_thumbnail or activity_timer:is_enabled() then
activity_timer:kill()
end
activity_timer:resume()
end
if time == last_seek_time then return end
last_seek_time = time
if not spawned then spawn(time) end
request_seek()
if not file_timer:is_enabled() then file_timer:resume() end
end
local function watch_changes()
if not dirty or not properties["video-out-params"] then return end
dirty = false
local old_w = effective_w
local old_h = effective_h
calc_dimensions()
local vf_reset = vf_string(filters_reset)
local rotate = properties["video-rotate"] or 0
local resized = old_w ~= effective_w or
old_h ~= effective_h or
last_vf_reset ~= vf_reset or
(last_rotate % 180) ~= (rotate % 180) or
par ~= last_par
if resized then
last_rotate = rotate
info(effective_w, effective_h)
elseif last_has_vid ~= has_vid and has_vid ~= 0 then
info(effective_w, effective_h)
end
if spawned then
if resized then
-- mpv doesn't allow us to change output size
local seek_time = last_seek_time
run("quit")
clear()
spawned = false
spawn(seek_time or mp.get_property_number("time-pos", 0))
file_timer:resume()
else
if rotate ~= last_rotate then
run("set video-rotate "..rotate)
end
local vf_runtime = vf_string(filters_runtime)
if vf_runtime ~= last_vf_runtime then
run("vf set "..vf_string(filters_all, true))
last_vf_runtime = vf_runtime
end
end
else
last_vf_runtime = vf_string(filters_runtime)
end
last_vf_reset = vf_reset
last_rotate = rotate
last_par = par
last_has_vid = has_vid
if not spawned and not disabled and options.spawn_first and resized then
spawn(mp.get_property_number("time-pos", 0))
file_timer:resume()
end
end
local function update_property(name, value)
properties[name] = value
end
local function update_property_dirty(name, value)
properties[name] = value
dirty = true
if name == "tone-mapping" then
last_tone_mapping = nil
end
end
local function update_tracklist(name, value)
-- current-tracks shim
for _, track in ipairs(value) do
if track.type == "video" and track.selected then
properties["current-tracks/video/image"] = track.image
properties["current-tracks/video/albumart"] = track.albumart
return
end
end
end
local function sync_changes(prop, val)
update_property(prop, val)
if val == nil then return end
if type(val) == "boolean" then
if prop == "vid" then
has_vid = 0
last_has_vid = 0
info(effective_w, effective_h)
clear()
return
end
val = val and "yes" or "no"
end
if prop == "vid" then
has_vid = 1
end
if not spawned then return end
run("set "..prop.." "..val)
dirty = true
end
local function file_load()
clear()
spawned = false
real_w, real_h = nil, nil
last_real_w, last_real_h = nil, nil
last_tone_mapping = nil
last_seek_time = nil
if info_timer then
info_timer:kill()
info_timer = nil
end
calc_dimensions()
info(effective_w, effective_h)
end
local function shutdown()
run("quit")
remove_thumbnail_files()
if os_name ~= "windows" then
os.remove(options.socket)
os.remove(options.socket..".run")
end
end
local function on_duration(prop, val)
allow_fast_seek = (val or 30) >= 30
end
mp.observe_property("current-tracks", "native", function(name, value)
if pre_0_33_0 then
mp.unobserve_property(update_tracklist)
pre_0_33_0 = false
end
update_property(name, value)
end)
mp.observe_property("track-list", "native", update_tracklist)
mp.observe_property("display-hidpi-scale", "native", update_property_dirty)
mp.observe_property("video-out-params", "native", update_property_dirty)
mp.observe_property("video-params", "native", update_property_dirty)
mp.observe_property("vf", "native", update_property_dirty)
mp.observe_property("tone-mapping", "native", update_property_dirty)
mp.observe_property("demuxer-via-network", "native", update_property)
mp.observe_property("stream-open-filename", "native", update_property)
mp.observe_property("macos-app-activation-policy", "native", update_property)
mp.observe_property("current-vo", "native", update_property)
mp.observe_property("video-rotate", "native", update_property)
mp.observe_property("path", "native", update_property)
mp.observe_property("vid", "native", sync_changes)
mp.observe_property("edition", "native", sync_changes)
mp.observe_property("duration", "native", on_duration)
mp.register_script_message("thumb", thumb)
mp.register_script_message("clear", clear)
mp.register_event("file-loaded", file_load)
mp.register_event("shutdown", shutdown)
mp.register_idle(watch_changes)

1
scripts/thumbfast.lua Symbolic link
View File

@@ -0,0 +1 @@
../submodules/thumbfast/thumbfast.lua

View File

@@ -1 +1 @@
../mpv-youtube-upnext/youtube-upnext.lua ../submodules/mpv-youtube-upnext/youtube-upnext.lua

View File

@@ -1 +1 @@
../ytdl-preload/ytdl-preload.lua ../submodules/ytdl-preload/ytdl-preload.lua

1407
shaders/ArtCNN_C4F16.glsl Normal file

File diff suppressed because it is too large Load Diff

1407
shaders/ArtCNN_C4F16_DS.glsl Normal file

File diff suppressed because it is too large Load Diff

3927
shaders/ArtCNN_C4F32.glsl Normal file

File diff suppressed because it is too large Load Diff

3927
shaders/ArtCNN_C4F32_DS.glsl Normal file

File diff suppressed because it is too large Load Diff

1
submodules/ModernZ Submodule

Submodule submodules/ModernZ added at cd23007c69

1
submodules/animecards Submodule

Submodule submodules/animecards added at ced1d30630

1
submodules/mpvacious Submodule

Submodule submodules/mpvacious added at 01c6adc825

1
submodules/thumbfast Submodule

Submodule submodules/thumbfast added at 9deb0733c4