diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 9f4d9d0..b4bbab8 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -18,6 +18,8 @@ jobs: python-version: 3.x - name: Install dependencies 🧰 + env: + PIP_ROOT_USER_ACTION: ignore run: | python -m pip install --upgrade pip pip install build @@ -26,7 +28,7 @@ jobs: run: python -m build - name: Publish package 🚀 - uses: Commandcracker/pypi-publish@32e78ea691b666534c641470e9d74e4deca05bcc + uses: Commandcracker/pypi-publish@8d13f542c4bb425036897e69001b670654c41a8d with: password: ${{ secrets.PYPI_API_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index b097a2a..d81ae1a 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,8 @@ Place your custom CSS in `user_config_path("gucken").joinpath("custom.css")` and - [ ] FIX TYPING SOMETIMES CAUSES CRASH - [ ] Syncplay on Android - [ ] More CLI args +- [ ] reverse proxy +- [ ] Chapters for VLC [Anime4k]: https://github.com/bloc97/Anime4K [MPV]: https://mpv.io/ diff --git a/pyproject.toml b/pyproject.toml index 34fce34..d810f58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ maintainers = [{name="Commandcracker"}] license = {file = "LICENSE.txt"} readme = "README.md" dependencies = [ - "textual>=0.60.1", + "textual>=0.62.0", "beautifulsoup4>=4.12.3", "httpx>=0.27.0", "pypresence>=4.3.0", diff --git a/src/gucken/__init__.py b/src/gucken/__init__.py index e1a267b..da80c81 100644 --- a/src/gucken/__init__.py +++ b/src/gucken/__init__.py @@ -1 +1 @@ -__version__ = "0.1.4" +__version__ = "0.1.5" diff --git a/src/gucken/gucken.py b/src/gucken/gucken.py index 8c04223..69988f4 100644 --- a/src/gucken/gucken.py +++ b/src/gucken/gucken.py @@ -2,8 +2,8 @@ import logging from asyncio import gather from atexit import register as register_atexit -from os import name as os_name -from os import remove +from os import remove, name as os_name +from os.path import join from pathlib import Path from random import choice from shutil import which @@ -56,7 +56,7 @@ from .provider.serienstream import SerienStreamProvider from .settings import gucken_settings_manager from .update import check -from .utils import detect_player, is_android +from .utils import detect_player, is_android, set_default_vlc_interface_cfg, get_vlc_intf_user_path def sort_favorite_lang( @@ -207,7 +207,7 @@ class GuckenApp(App): TITLE = "Gucken TUI" # TODO: color theme https://textual.textualize.io/guide/design/#designing-with-colors - CSS_PATH = ["gucken.css"] + CSS_PATH = [join("resources", "gucken.css")] custom_css = user_config_path("gucken").joinpath("custom.css") if custom_css.exists(): CSS_PATH.append(custom_css) @@ -312,7 +312,7 @@ def compose(self) -> ComposeResult: Select.BLANK if player == "AutomaticPlayer" else player ), ) - with Collapsible(title="ani-skip (only for MPV)", collapsed=False): + with Collapsible(title="ani-skip (only for MPV and VLC)", collapsed=False): yield RadioButton( "Skip opening", id="ani_skip_opening", @@ -324,7 +324,7 @@ def compose(self) -> ComposeResult: value=settings["ani_skip"]["skip_ending"], ) yield RadioButton( - "Get chapters", + "Get chapters (only MPV)", id="ani_skip_chapters", value=settings["ani_skip"]["chapters"], ) @@ -460,7 +460,8 @@ async def disable_RPC(self): self.RPC = None # TODO: https://textual.textualize.io/guide/workers/#thread-workers - @work(exclusive=True) + # TODO: Exit on error when debug = true + @work(exclusive=True, exit_on_error=False) async def lookup_anime(self, keyword: str) -> None: search_providers = [] if self.query_one("#aniworld_to", Checkbox).value: @@ -659,12 +660,21 @@ async def update(): self.app.call_later(update) + if self._debug: + logs_path = user_log_path("gucken", ensure_exists=True) + if isinstance(_player, MPVPlayer): + args.append("--log-file=" + str(logs_path.joinpath("mpv.log"))) + elif isinstance(_player, VLCPlayer): + args.append("--file-logging") + args.append("--log-verbose=3") + args.append("--logfile=" + str(logs_path.joinpath("vlc.log"))) + chapters_file = None # TODO: cache more # TODO: Support based on mpv # TODO: recover start --start=00:56 - if isinstance(_player, MPVPlayer): + if isinstance(_player, MPVPlayer) or isinstance(_player, VLCPlayer): ani_skip_opening = self.query_one("#ani_skip_opening", RadioButton).value ani_skip_ending = self.query_one("#ani_skip_ending", RadioButton).value ani_skip_chapters = self.query_one("#ani_skip_chapters", RadioButton).value @@ -674,29 +684,65 @@ async def update(): series_search_result.name, index + 1 ) if timings: - if ani_skip_chapters: - chapters_file = generate_chapters_file(timings) + if isinstance(_player, MPVPlayer): + if ani_skip_chapters: + chapters_file = generate_chapters_file(timings) + + def delete_chapters_file(): + try: + remove(chapters_file.name) + except FileNotFoundError: + pass + + register_atexit(delete_chapters_file) + + args.append(get_chapters_file_mpv_option(chapters_file.name)) + + if ani_skip_opening: + args.append(opening_timings_to_mpv_option(timings)) + + if ani_skip_ending: + args.append(ending_timings_to_mpv_option(timings)) + + args.append("--script=" + str(Path(__file__).parent.joinpath("resources", "mpv_skip.lua"))) + + if isinstance(_player, VLCPlayer): + # cant use --lua-config because it would override syncplay cfg + # cant use --extraintf and --lua-intf because it is already used by syncplay + """ + args = [ + "vlc", + "--extraintf=luaintf", + "--lua-intf=skip", + "--lua-config=skip={" + f"op_start={op_start},op_end={op_end},ed_start={ed_start},ed_end={ed_end}" +"}", + url + ] + """ + prepend_data = ["-- Generated"] + + if ani_skip_opening: + prepend_data.append(set_default_vlc_interface_cfg("op_start", timings["op_start_time"])) + prepend_data.append(set_default_vlc_interface_cfg("op_end", timings["op_end_time"])) + + if ani_skip_ending: + prepend_data.append(set_default_vlc_interface_cfg("ed_start", timings["ed_start_time"])) + prepend_data.append(set_default_vlc_interface_cfg("ed_end", timings["ed_end_time"])) - def delete_chapters_file(): - try: - remove(chapters_file.name) - except FileNotFoundError: - pass + prepend_data.append("-- Generated\n") - register_atexit(delete_chapters_file) + vlc_intf_user_path = get_vlc_intf_user_path(_player.executable).vlc_intf_user_path + Path(vlc_intf_user_path).mkdir(mode=0o755, parents=True, exist_ok=True) - args.append(get_chapters_file_mpv_option(chapters_file.name)) + vlc_skip_plugin = Path(__file__).parent.joinpath("resources", "vlc_skip.lua") + copyTo = join(vlc_intf_user_path, "vlc_skip.lua") - if ani_skip_opening: - args.append(opening_timings_to_mpv_option(timings)) + with open(vlc_skip_plugin, 'r') as f: + original_content = f.read() - if ani_skip_ending: - args.append(ending_timings_to_mpv_option(timings)) + with open(copyTo, 'w') as f: + f.write("\n".join(prepend_data) + original_content) - args.append("--script=" + str(Path(__file__).parent.joinpath("skip.lua"))) - if self._debug: - logs_path = user_log_path("gucken", ensure_exists=True) - args.append("--log-file=" + str(logs_path.joinpath("mpv.log"))) + args.append("--control=luaintf{intf=vlc_skip}") if syncplay: # TODO: make work with flatpak @@ -746,7 +792,7 @@ def delete_chapters_file(): resume_time = None - # only if mpv + # only if mpv WIP while not self.app._exit: output = process.stderr.readline() if process.poll() is not None: diff --git a/src/gucken/default_settings.toml b/src/gucken/resources/default_settings.toml similarity index 100% rename from src/gucken/default_settings.toml rename to src/gucken/resources/default_settings.toml diff --git a/src/gucken/gucken.css b/src/gucken/resources/gucken.css similarity index 100% rename from src/gucken/gucken.css rename to src/gucken/resources/gucken.css diff --git a/src/gucken/resources/mpv_skip.lua b/src/gucken/resources/mpv_skip.lua new file mode 100644 index 0000000..0714899 --- /dev/null +++ b/src/gucken/resources/mpv_skip.lua @@ -0,0 +1,42 @@ +local mpv_utils = require("mp.utils") + +-- Stop script if skip.lua is inside scripts folder +local scripts_dir = mp.find_config_file("scripts") +if mpv_utils.file_info(mpv_utils.join_path(scripts_dir, "skip.lua")) ~= nil then + mp.msg.info("Disabling, another skip.lua is already present in scripts dir") + return +end + +local mpv_options = require("mp.options") + +local options = { + op_start = 0, + op_end = 0, + ed_start = 0, + ed_end = 0, +} +mpv_options.read_options(options, "skip") + +local skipped_op = false +local skipped_ed = false + +local function check_skip() + local current_time = mp.get_property_number("time-pos") + if not current_time then + return + end + + -- Opening + if not skipped_op and current_time >= options.op_start and current_time < options.op_end then + mp.set_property_number("time-pos", options.op_end) + skipped_op = true + end + + -- Ending + if not skipped_ed and current_time >= options.ed_start and current_time < options.ed_end then + mp.set_property_number("time-pos", options.ed_end) + skipped_ed = true + end +end + +mp.observe_property("time-pos", "number", check_skip) diff --git a/src/gucken/resources/vlc_skip.lua b/src/gucken/resources/vlc_skip.lua new file mode 100755 index 0000000..9be7f20 --- /dev/null +++ b/src/gucken/resources/vlc_skip.lua @@ -0,0 +1,150 @@ +-- Returns time in microseconds or nil if not found +local function get_time() + local input = vlc.object.input() + if input then + return vlc.var.get(input, "time") + end +end + +-- Returns true if successful and false if not +local function set_time(microseconds) + local input = vlc.object.input() + if input then + vlc.var.set(input, "time", microseconds) + return true + end + return false +end + +-- Get timings form options and converts them to microseconds +local function get_time_option(key) + time = tonumber(config[key]) + if time then + return time * 1000000 + end +end + +-- Get timings from options +local options = { + op_start = get_time_option("op_start"), + op_end = get_time_option("op_end"), + ed_start = get_time_option("ed_start"), + ed_end = get_time_option("ed_end"), +} + +-- Vals to only skip once +local skipped_op = false +local skipped_ed = false + +-- Check if booth op times are given +local has_op = false +if options.op_start and options.op_end then + has_op = true +else + -- No op = already skipped + skipped_op = true +end +-- Check if booth ed times are given +local has_ed = false +if options.ed_start and options.ed_end then + has_ed = true +else + -- No ed = already skipped + skipped_ed = true +end + +-- Exit if no timings are specified +if not has_op and not has_ed then + return +end + +while true do + local time = get_time() + + if time then + -- This is captured by gucken + --print("TIME:", time) + + if not skipped_op and time >= options.op_start and time < options.op_end then + skipped_op = set_time(options.op_end) + end + + if not skipped_ed and time >= options.ed_start and time < options.ed_end then + skipped_ed = set_time(options.ed_end) + end + end + + -- Exit when all skips are finished + if skipped_op == true and skipped_ed == true then + return + end + + vlc.misc.mwait(vlc.misc.mdate() + 2500) -- Don't waste processor time +end + +--[[ load form one script +-- TODO: only load syncplay when it should load + +-- Add intf path to package.path, so require can find syncplay +local file_path = debug.getinfo(1, "S").source:sub(2) +local separator = '/' +if string.find(file_path, '\\') then separator = '\\' end + +local parts = {} +for part in string.gmatch(file_path, "[^" .. separator .. "]+") do + table.insert(parts, part) +end +table.remove(parts, #parts) +local intf_path = table.concat(parts, separator) + +package.path = intf_path..separator.."?.lua;"..package.path + +-- Add coroutine.yield() to custom mwait +original_mwait = vlc.misc.mwait + +function custom_mwait(microseconds) + coroutine.yield() + -- booth syncplay and skip wait 2500 microseconds, + -- so we just halt that on booth of them and then they still wait the sme time + original_mwait(microseconds-1250) +end + +-- Inject the custom mwait function +_G = setmetatable({}, { + __index = function(self, key) + if key == "vlc" then + return setmetatable({}, { + __index = function(self, key) + if key == "misc" then + return setmetatable({}, { + __index = function(self, key) + if key == "mwait" then return custom_mwait end + return self[key] + end, + }) + end + return self[key] + end, + }) + end + return self[key] + end, +}) + +-- TODO: get and inject syncplay config +local function syncplay() require("syncplay") end + +local skip_coroutine = coroutine.create(main) +local syncplay_coroutine = coroutine.create(syncplay) + +while coroutine.status(skip_coroutine) ~= "dead" or coroutine.status(syncplay_coroutine) ~= "dead" do + if coroutine.status(skip_coroutine) ~= "dead" then + coroutine.resume(skip_coroutine) + end + if coroutine.status(syncplay_coroutine) ~= "dead" then + coroutine.resume(syncplay_coroutine) + end +end + +-- TODO: fix vlc sometimes not quitting +]] diff --git a/src/gucken/settings.py b/src/gucken/settings.py index 3286cec..1d76c02 100644 --- a/src/gucken/settings.py +++ b/src/gucken/settings.py @@ -79,6 +79,6 @@ class GuckenSettingsManager(Singleton, SettingsManager): gucken_settings_manager = GuckenSettingsManager( - default_settings_file=Path(__file__).parent.joinpath("default_settings.toml"), + default_settings_file=Path(__file__).parent.joinpath("resources", "default_settings.toml"), settings_file=user_config_path("gucken").joinpath("settings.toml"), ) diff --git a/src/gucken/skip.lua b/src/gucken/skip.lua deleted file mode 100644 index 1f46c64..0000000 --- a/src/gucken/skip.lua +++ /dev/null @@ -1,40 +0,0 @@ -local mpv_utils = require("mp.utils") - --- Stop script if skip.lua is inside scripts folder -local scripts_dir = mp.find_config_file("scripts") -if mpv_utils.file_info(mpv_utils.join_path(scripts_dir, "skip.lua")) ~= nil then - mp.msg.info("Disabling, another skip.lua is already present in scripts dir") - return -end - -local mpv_options = require("mp.options") - -local options = { - op_start = 0, - op_end = 0, - ed_start = 0, - ed_end = 0 -} -mpv_options.read_options(options, "skip") - -local skipped_op = false -local skipped_ed = false - -local function check_skip() - local current_time = mp.get_property_number("time-pos") - if not current_time then return end - - -- Opening - if not skipped_op and current_time >= options.op_start and current_time < options.op_end then - mp.set_property_number("time-pos", options.op_end) - skipped_op = true - end - - -- Ending - if not skipped_ed and current_time >= options.ed_start and current_time < options.ed_end then - mp.set_property_number("time-pos", options.ed_end) - skipped_ed = true - end -end - -mp.observe_property("time-pos", "number", check_skip) diff --git a/src/gucken/utils.py b/src/gucken/utils.py index 26b35f6..8be7576 100644 --- a/src/gucken/utils.py +++ b/src/gucken/utils.py @@ -1,5 +1,8 @@ +import os import sys + from typing import Union +from typing import NamedTuple from .player.android import AndroidChoosePlayer from .player.common import Player @@ -33,3 +36,62 @@ def detect_player() -> Union[Player, None]: return p() return None + + +def is_windows(): + return sys.platform.startswith("win") + + +def is_linux(): + return sys.platform.startswith("linux") + + +def is_mac_os(): + return sys.platform.startswith("darwin") + + +def is_bsd(): + return "freebsd" in sys.platform or sys.platform.startswith("dragonfly") + + +class VLCPaths(NamedTuple): + vlc_intf_path: str + vlc_intf_user_path: str + vlc_module_path: str + + +def get_vlc_intf_user_path(player_path: str) -> VLCPaths: + if is_linux(): + if 'snap' in player_path: + vlc_intf_path = '/snap/vlc/current/usr/lib/vlc/lua/intf/' + vlc_intf_user_path = os.path.join(os.getenv('HOME', '.'), "snap/vlc/current/.local/share/vlc/lua/intf/") + else: + vlc_intf_path = "/usr/lib/vlc/lua/intf/" + vlc_intf_user_path = os.path.join(os.getenv('HOME', '.'), ".local/share/vlc/lua/intf/") + elif is_mac_os(): + vlc_intf_path = "/Applications/VLC.app/Contents/MacOS/share/lua/intf/" + vlc_intf_user_path = os.path.join( + os.getenv('HOME', '.'), "Library/Application Support/org.videolan.vlc/lua/intf/") + elif is_bsd(): + # *BSD ports/pkgs install to /usr/local by default. + # This should also work for all the other BSDs, such as OpenBSD or DragonFly. + vlc_intf_path = "/usr/local/lib/vlc/lua/intf/" + vlc_intf_user_path = os.path.join(os.getenv('HOME', '.'), ".local/share/vlc/lua/intf/") + elif "vlcportable.exe" in player_path.lower(): + vlc_intf_path = os.path.dirname(player_path).replace("\\", "/") + "/App/vlc/lua/intf/" + vlc_intf_user_path = vlc_intf_path + else: + vlc_intf_path = os.path.dirname(player_path).replace("\\", "/") + "/lua/intf/" + vlc_intf_user_path = os.path.join(os.getenv('APPDATA', '.'), "VLC\\lua\\intf\\") + + vlc_module_path = vlc_intf_path + "modules/?.luac" + + return VLCPaths( + vlc_intf_path=vlc_intf_path, + vlc_intf_user_path=vlc_intf_user_path, + vlc_module_path=vlc_module_path + ) + + +def set_default_vlc_interface_cfg(key: str, value: any) -> str: + return f'config["{key}"] = config["{key}"] or ' + str(value) or "nil" diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..394e884 --- /dev/null +++ b/stylua.toml @@ -0,0 +1 @@ +indent_type = "Spaces"