Compare commits
34 Commits
1c6904a8a5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
6eadea6f00
|
|||
|
05adde2559
|
|||
|
7579628faf
|
|||
|
5581723522
|
|||
|
b6390baec6
|
|||
|
21998241ba
|
|||
|
57f6a5db30
|
|||
|
5a6ac4bb3a
|
|||
|
5f08f09b2c
|
|||
|
9819a64f01
|
|||
|
73a305af0b
|
|||
|
9ac84aba18
|
|||
|
cfc6ac22e5
|
|||
|
e4afe79832
|
|||
|
ca0899d233
|
|||
|
deb591481e
|
|||
|
472899b680
|
|||
|
2f3ccc60fc
|
|||
|
02d3c00229
|
|||
|
f3a4afcf76
|
|||
|
087f16dfd0
|
|||
|
0b9f814eca
|
|||
|
efb9737e14
|
|||
|
56e477bff1
|
|||
|
04fabf6356
|
|||
|
d9747459d8
|
|||
|
33ea15228e
|
|||
|
1cd68861f5
|
|||
|
2c90b30993
|
|||
|
382c29ba42
|
|||
| 37b67be69b | |||
| 2ed3918aca | |||
| f0237be035 | |||
|
5260564aaf
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,3 +2,5 @@ immersive-data
|
||||
.ytdl_preload/
|
||||
.ytdl-preload/
|
||||
scripts/anilistUpdater/anilistToken.txt
|
||||
.watch-later
|
||||
state
|
||||
|
||||
33
.gitmodules
vendored
33
.gitmodules
vendored
@@ -1,18 +1,31 @@
|
||||
[submodule "mpv-youtube-upnext"]
|
||||
path = mpv-youtube-upnext
|
||||
path = submodules/mpv-youtube-upnext
|
||||
url = https://github.com/ksyasuda/mpv-youtube-upnext
|
||||
[submodule "mpv-youtube-queue"]
|
||||
path = mpv-youtube-queue
|
||||
path = submodules/mpv-youtube-queue
|
||||
url = https://github.com/ksyasuda/mpv-youtube-queue
|
||||
[submodule "ModernZ"]
|
||||
path = ModernZ
|
||||
path = submodules/ModernZ
|
||||
url = git@github.com:Samillion/ModernZ.git
|
||||
[submodule "ytdl-preload"]
|
||||
path = ytdl-preload
|
||||
path = submodules/ytdl-preload
|
||||
url = git@gist.github.com:17d90e3deeb35b5f75e55adb19098f58.git
|
||||
[submodule "autosubsync-mpv"]
|
||||
path = autosubsync-mpv
|
||||
url = git@github.com:Ajatt-Tools/autosubsync-mpv.git
|
||||
[submodule "scripts/autosubsync-mpv"]
|
||||
path = scripts/autosubsync-mpv
|
||||
url = git@github.com:Ajatt-Tools/autosubsync-mpv.git
|
||||
[submodule "mpv-anilist-updater"]
|
||||
path = submodules/mpv-anilist-updater
|
||||
url = git@github.com:AzuredBlue/mpv-anilist-updater.git
|
||||
[submodule "thumbfast"]
|
||||
path = submodules/thumbfast
|
||||
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
2
.prettierrc
Normal file
@@ -0,0 +1,2 @@
|
||||
trailingComma: "es5"
|
||||
singleQuote: true
|
||||
1
ModernZ
1
ModernZ
Submodule ModernZ deleted from 2d5537aa72
78
input.conf
78
input.conf
@@ -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+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+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+K cycle-values keep-open "yes" "no"
|
||||
ctrl+r script-binding reload-scripts
|
||||
ctrl+s script_binding autosubsync-menu
|
||||
|
||||
# {{{ sponsorblock
|
||||
ctrl+g script-binding sponsorblock/set_segment
|
||||
ctrl+G script-binding sponsorblock/submit_segment
|
||||
ctrl+h script-binding sponsorablock/upvote_setment
|
||||
ctrl+H script-binding sponsorablock/downvote_segment
|
||||
ctrl+< script-binding sponsorblock/upvote_setment
|
||||
ctrl+> script-binding sponsorblock/downvote_segment
|
||||
#}}}
|
||||
|
||||
# {{{ anilist
|
||||
ctrl+A script-binding update_anilist
|
||||
ctrl+B script-binding launch_anilist
|
||||
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
243
mac.conf
Normal 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
242
mpv.conf
@@ -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; 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
|
||||
|
||||
ontop=yes
|
||||
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
|
||||
# geometry=50%
|
||||
autofit=50%
|
||||
volume=75
|
||||
audio-spdif=ac3,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/FSRCNNX.glsl:~~/shaders/FSR.glsl:~~/shaders/NVScaler.glsl:~~/shaders/CAS-scaled.glsl"
|
||||
# glsl-shaders="~~/shaders/FSR.glsl"
|
||||
# glsl-shaders="~~/shaders/FSRCNNX.glsl:~~/shaders/FSR.glsl:~~/shaders/NVScaler.glsl:~~/shaders/CAS-scaled.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.
|
||||
opengl-early-flush=no
|
||||
@@ -172,46 +31,79 @@ ytdl-format=bestvideo+bestaudio/best
|
||||
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
|
||||
profile=high-quality
|
||||
blend-subtitles=video
|
||||
# GPU OPTIONS
|
||||
vo=gpu-next
|
||||
hwdec=nvdec-copy
|
||||
scale=bicubic
|
||||
dscale=bicubic
|
||||
hwdec=nvdec
|
||||
gpu-api=vulkan
|
||||
scale=ewa_lanczossharp
|
||||
dscale=catmull_rom
|
||||
cscale=bicubic
|
||||
tscale=oversample
|
||||
interpolation=yes
|
||||
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
|
||||
# vo=gpu
|
||||
# gpu-api=opengl
|
||||
# gpu-context=wayland
|
||||
# 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
|
||||
# ao=pule,pipewire
|
||||
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]
|
||||
target-colorspace-hint=yes
|
||||
gpu-api=vulkan
|
||||
@@ -230,14 +122,18 @@ keepaspect=no
|
||||
|
||||
[immersion]
|
||||
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-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-remove=sponsor
|
||||
ytdl-format=bestvideo+bestaudio/best
|
||||
# get subtitles for videos automatically
|
||||
sub-auto=fuzzy
|
||||
slang=ja,jpn
|
||||
slang=ja,jpn,JA,JPN,ja.hi,ja.*
|
||||
alang=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
BIN
mpv_websocket-mac
Executable file
Binary file not shown.
11
script-opts/anilistUpdater.conf
Normal file
11
script-opts/anilistUpdater.conf
Normal 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
107
script-opts/animecards.conf
Normal 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)
|
||||
@@ -19,13 +19,13 @@ audio_subsync_tool=ffsubsync
|
||||
|
||||
# 2. Preferred tool for syncing to another subtitle.
|
||||
# altsub_subsync_tool=ask
|
||||
altsub_subsync_tool=ffsubsync
|
||||
# altsub_subsync_tool=alass
|
||||
# altsub_subsync_tool=ffsubsync
|
||||
altsub_subsync_tool=alass
|
||||
|
||||
# Unload old subs (yes,no)
|
||||
# After retiming, tell mpv to forget the original subtitle track.
|
||||
# unload_old_sub=yes
|
||||
unload_old_sub=no
|
||||
unload_old_sub=yes
|
||||
# unload_old_sub=no
|
||||
|
||||
# Overwrite the original subtitle file.
|
||||
# Replace the old subtitle file with the retimed file.
|
||||
|
||||
22
script-opts/immersion-tracker.conf
Normal file
22
script-opts/immersion-tracker.conf
Normal 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
|
||||
|
||||
@@ -30,6 +30,6 @@ menu_timeout=5
|
||||
show_errors=yes
|
||||
ytdlp_file_format=mp4
|
||||
ytdlp_output_template=%(uploader)s/%(title)s.%(ext)s
|
||||
use_history_db=no
|
||||
use_history_db=yes
|
||||
backend_host=http://localhost
|
||||
backend_port=42069
|
||||
|
||||
331
script-opts/subs2srs.conf
Normal file
331
script-opts/subs2srs.conf
Normal 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
|
||||
1
script-opts/ytdl_preload.conf
Normal file
1
script-opts/ytdl_preload.conf
Normal file
@@ -0,0 +1 @@
|
||||
temp=/tmp/ytdl-preload
|
||||
@@ -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()
|
||||
1
scripts/anilistUpdater/anilistUpdater.py
Symbolic link
1
scripts/anilistUpdater/anilistUpdater.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../submodules/mpv-anilist-updater/anilistUpdater/anilistUpdater.py
|
||||
@@ -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)
|
||||
1
scripts/anilistUpdater/main.lua
Symbolic link
1
scripts/anilistUpdater/main.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../../submodules/mpv-anilist-updater/anilistUpdater/main.lua
|
||||
1
scripts/animecards
Symbolic link
1
scripts/animecards
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/animecards/animecards
|
||||
@@ -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)
|
||||
Submodule scripts/autosubsync-mpv deleted from 125ac13d1b
1
scripts/autosubsync-mpv
Symbolic link
1
scripts/autosubsync-mpv
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/autosubsync-mpv
|
||||
1
scripts/immersion-tracker
Symbolic link
1
scripts/immersion-tracker
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/immersion-tracker
|
||||
388
scripts/jimaku.js
Normal file
388
scripts/jimaku.js
Normal 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
|
||||
);
|
||||
@@ -1 +1 @@
|
||||
../ModernZ/modernz.lua
|
||||
../submodules/ModernZ/modernz.lua
|
||||
@@ -1 +1 @@
|
||||
../mpv-youtube-queue/mpv-youtube-queue.lua
|
||||
../submodules/mpv-youtube-queue/mpv-youtube-queue.lua
|
||||
1
scripts/subs2srs
Symbolic link
1
scripts/subs2srs
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/mpvacious
|
||||
@@ -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
1
scripts/thumbfast.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/thumbfast/thumbfast.lua
|
||||
@@ -1 +1 @@
|
||||
../mpv-youtube-upnext/youtube-upnext.lua
|
||||
../submodules/mpv-youtube-upnext/youtube-upnext.lua
|
||||
@@ -1 +1 @@
|
||||
../ytdl-preload/ytdl-preload.lua
|
||||
../submodules/ytdl-preload/ytdl-preload.lua
|
||||
1407
shaders/ArtCNN_C4F16.glsl
Normal file
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
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
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
3927
shaders/ArtCNN_C4F32_DS.glsl
Normal file
File diff suppressed because it is too large
Load Diff
1
submodules/ModernZ
Submodule
1
submodules/ModernZ
Submodule
Submodule submodules/ModernZ added at cd23007c69
1
submodules/animecards
Submodule
1
submodules/animecards
Submodule
Submodule submodules/animecards added at ced1d30630
1
submodules/autosubsync-mpv
Submodule
1
submodules/autosubsync-mpv
Submodule
Submodule submodules/autosubsync-mpv added at 125ac13d1b
1
submodules/immersion-tracker
Submodule
1
submodules/immersion-tracker
Submodule
Submodule submodules/immersion-tracker added at 2d8d139cb0
1
submodules/mpv-anilist-updater
Submodule
1
submodules/mpv-anilist-updater
Submodule
Submodule submodules/mpv-anilist-updater added at a5851c4c0e
1
submodules/mpvacious
Submodule
1
submodules/mpvacious
Submodule
Submodule submodules/mpvacious added at 01c6adc825
1
submodules/thumbfast
Submodule
1
submodules/thumbfast
Submodule
Submodule submodules/thumbfast added at 9deb0733c4
Reference in New Issue
Block a user