From 9e091a44a483037f88c49828801f0db894fdead3 Mon Sep 17 00:00:00 2001 From: Commandcracker Date: Sun, 26 May 2024 17:12:06 +0200 Subject: [PATCH] ani-skip support out of WIP --- README.md | 5 +- pyproject.toml | 6 +- src/gucken/__init__.py | 2 +- src/gucken/aniskip.py | 111 +++++-------- src/gucken/gucken.py | 61 +++---- src/gucken/resources/gucken.css | 6 +- .../{mpv_skip.lua => mpv_gucken.lua} | 2 +- src/gucken/resources/vlc_gucken.lua | 57 +++++++ src/gucken/resources/vlc_skip.lua | 150 ------------------ test/fuzzy.py | 71 --------- test/presence.py | 3 + test/rome.py | 24 --- 12 files changed, 139 insertions(+), 359 deletions(-) rename src/gucken/resources/{mpv_skip.lua => mpv_gucken.lua} (90%) create mode 100644 src/gucken/resources/vlc_gucken.lua delete mode 100755 src/gucken/resources/vlc_skip.lua delete mode 100644 test/fuzzy.py delete mode 100644 test/rome.py diff --git a/README.md b/README.md index d81ae1a..2f7cba6 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,9 @@ termux-setup-storage - [x] Descriptions - [x] Watching - [x] Automatically start next episode - - [x] Discord Presence **WIP** + - [x] Discord Presence **Very WIP** - [MPV] only - - [X] [ani-skip](https://github.com/synacktraa/ani-skip) support **Very WIP** + - [X] [ani-skip](https://github.com/synacktraa/ani-skip) support - [x] [Syncplay](https://github.com/Syncplay/syncplay) support (almost out of WIP) - [ ] Remember watch time **WIP** - [ ] Remember completed Episodes (and series) @@ -205,7 +205,6 @@ Place your custom CSS in `user_config_path("gucken").joinpath("custom.css")` and - [ ] Proper error handling - [ ] Logging and Crash reports - [ ] Pre-fetching -- [ ] improve [ani-skip](https://github.com/synacktraa/ani-skip) support - [ ] Use something like opencv to time match a sub from aniworld with a high quality video form another site. - [ ] Image preview (Kitty protocol, iterm protocol, Sixel, textual-web) - [ ] Support textual-web diff --git a/pyproject.toml b/pyproject.toml index d810f58..f9ab6f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,13 +7,15 @@ maintainers = [{name="Commandcracker"}] license = {file = "LICENSE.txt"} readme = "README.md" dependencies = [ - "textual>=0.62.0", + "textual>=0.63.3", "beautifulsoup4>=4.12.3", "httpx>=0.27.0", "pypresence>=4.3.0", "packaging>=24.0", "platformdirs>=4.2.2", - "toml>=0.10.2" + "toml>=0.10.2", + "fuzzywuzzy>=0.18.0", + "levenshtein>=0.25.1" #"yt-dlp>=2024.4.9", #"mpv>=1.0.6", #"httpx[socks]", diff --git a/src/gucken/__init__.py b/src/gucken/__init__.py index 517456c..bd8d54b 100644 --- a/src/gucken/__init__.py +++ b/src/gucken/__init__.py @@ -1 +1 @@ -__version__ = "0.1.8" +__version__ = "0.1.9" diff --git a/src/gucken/aniskip.py b/src/gucken/aniskip.py index 3f3405b..470dca9 100644 --- a/src/gucken/aniskip.py +++ b/src/gucken/aniskip.py @@ -1,114 +1,85 @@ -from difflib import SequenceMatcher from tempfile import NamedTemporaryFile from typing import Union +from dataclasses import dataclass +from fuzzywuzzy import process from httpx import AsyncClient from .tracker.myanimelist import search +from .rome import replace_roman_numerals -# TODO: improve fuzzy - -def fuzzy_search(pattern, possibilities, threshold=0.6): - matches = [] - for word in possibilities: - ratio = SequenceMatcher(None, pattern, word).ratio() - if ratio >= threshold: - matches.append((word, ratio)) - return matches - - -def fuzzy_sort(pattern, possibilities): - return sorted( - possibilities, - key=lambda x: SequenceMatcher(None, pattern, x).ratio(), - reverse=True, - ) +@dataclass +class SkipTimes: + op_start: float + op_end: float + ed_start: float + ed_end: float async def get_timings_from_id( anime_id: int, episode_number: int -) -> Union[dict[str, float], None]: - async with AsyncClient(verify=False) as client: +) -> Union[SkipTimes, None]: + async with (AsyncClient(verify=False) as client): response = await client.get( f"https://api.aniskip.com/v1/skip-times/{anime_id}/{episode_number}?types=op&types=ed" ) json = response.json() if json.get("found") is not True: - return None - op_start_time = 0 - op_end_time = 0 - ed_start_time = 0 - ed_end_time = 0 + return + op_start = 0 + op_end = 0 + ed_start = 0 + ed_end = 0 for result in json["results"]: skip_type = result["skip_type"] start_time = result["interval"]["start_time"] end_time = result["interval"]["end_time"] if skip_type == "op": - op_start_time = start_time - op_end_time = end_time + op_start = start_time + op_end = end_time if skip_type == "ed": - ed_start_time = start_time - ed_end_time = end_time - return { - "op_start_time": float(op_start_time), - "op_end_time": float(op_end_time), - "ed_start_time": float(ed_start_time), - "ed_end_time": float(ed_end_time), - } + ed_start = start_time + ed_end = end_time + return SkipTimes( + op_start=float(op_start), + op_end=float(op_end), + ed_start=float(ed_start), + ed_end=float(ed_end) + ) async def get_timings_from_search( keyword: str, episode_number: int -) -> Union[dict[str, float], None]: - # TODO: improve search +) -> Union[SkipTimes, None]: myanimelist_search_result = await search(keyword) animes = {} for anime in myanimelist_search_result["categories"][0]["items"]: - animes[anime["name"]] = anime["id"] - search_results = fuzzy_search(keyword, animes) - if len(search_results) > 0: - name = search_results[0][0] - anime_id = animes[name] + animes[anime["id"]] = replace_roman_numerals(anime["name"]) + search_result = process.extractOne(replace_roman_numerals(keyword), animes, score_cutoff=50) + if search_result is not None: + anime_id = search_result[2] return await get_timings_from_id(anime_id, episode_number) return None -def opening_timings_to_mpv_option(timings=dict[str, float]) -> str: - op_start_time = timings["op_start_time"] - op_end_time = timings["op_end_time"] - return f"--script-opts-add=skip-op_start={op_start_time},skip-op_end={op_end_time}" - - -def ending_timings_to_mpv_option(timings=dict[str, float]) -> str: - ed_start_time = timings["ed_start_time"] - ed_end_time = timings["ed_end_time"] - return f"--script-opts-add=skip-ed_start={ed_start_time},skip-ed_end={ed_end_time}" - - def chapter(start: float, end: float, title: str) -> str: return f"\n[CHAPTER]\nTIMEBASE=1/1000\nSTART={int(start * 1000)}\nEND={int(end * 1000)}\nTITLE={title}\n" -def get_chapters_file_content(timings=dict[str, float]) -> str: - op_start_time = timings["op_start_time"] - op_end_time = timings["op_end_time"] - ed_start_time = timings["ed_start_time"] - ed_end_time = timings["ed_end_time"] - return ( - ";FFMETADATA1" - + chapter(op_start_time, op_end_time, "Opening") - + chapter(ed_start_time, ed_end_time, "Ending") - + chapter(op_end_time, ed_start_time, "Episode") - ) +def get_chapters_file_content(timings: SkipTimes) -> str: + string_builder = [";FFMETADATA1"] + if timings.op_start != timings.op_end: + string_builder.append(chapter(timings.op_start, timings.op_end, "Opening")) + if timings.ed_start != timings.ed_end: + string_builder.append(chapter(timings.ed_start, timings.ed_end, "Ending")) + if timings.op_end != 0 and timings.ed_start != 0: + string_builder.append(chapter(timings.op_end, timings.ed_start, "Episode")) + return "".join(string_builder) -def generate_chapters_file(timings=dict[str, float]) -> NamedTemporaryFile: +def generate_chapters_file(timings: SkipTimes) -> NamedTemporaryFile: temp_file = NamedTemporaryFile(mode="w", prefix="gucken-", delete=False) temp_file.write(get_chapters_file_content(timings)) temp_file.close() return temp_file - - -def get_chapters_file_mpv_option(path: str) -> str: - return f"--chapters-file={path}" diff --git a/src/gucken/gucken.py b/src/gucken/gucken.py index b4fd452..f179f1e 100644 --- a/src/gucken/gucken.py +++ b/src/gucken/gucken.py @@ -1,3 +1,6 @@ +import warnings +warnings.filterwarnings('ignore', message='Using slow pure-python SequenceMatcher. Install python-Levenshtein to remove this warning') + import argparse import logging from asyncio import gather @@ -25,7 +28,6 @@ Checkbox, Collapsible, DataTable, - Footer, Header, Input, Label, @@ -40,10 +42,7 @@ from .aniskip import ( generate_chapters_file, - get_chapters_file_mpv_option, - get_timings_from_search, - opening_timings_to_mpv_option, - ending_timings_to_mpv_option + get_timings_from_search ) from .custom_widgets import SortableTable from .hoster._hosters import hoster @@ -481,6 +480,7 @@ async def lookup_anime(self, keyword: str) -> None: final_results.append(e) # TODO: Sort final_results with fuzzy-sort + # from fuzzywuzzy import process process.extract() if len(final_results) > 0: self.current = final_results for series in final_results: @@ -681,7 +681,7 @@ async def update(): if ani_skip_opening or ani_skip_ending or ani_skip_chapters: timings = await get_timings_from_search( - series_search_result.name, index + 1 + series_search_result.name + " " + str(episode.season), episode.episode_number ) if timings: if isinstance(_player, MPVPlayer): @@ -695,54 +695,42 @@ def delete_chapters_file(): pass register_atexit(delete_chapters_file) + args.append(f"--chapters-file={chapters_file.name}") - args.append(get_chapters_file_mpv_option(chapters_file.name)) - + script_opts = [] if ani_skip_opening: - args.append(opening_timings_to_mpv_option(timings)) - + script_opts.append(f"skip-op_start={timings.op_start}") + script_opts.append(f"skip-op_end={timings.op_end}") if ani_skip_ending: - args.append(ending_timings_to_mpv_option(timings)) + script_opts.append(f"skip-ed_start={timings.ed_start}") + script_opts.append(f"skip-ed_end={timings.ed_end}") + if len(script_opts) > 0: + args.append(f"--script-opts={','.join(script_opts)}") - args.append("--script=" + str(Path(__file__).parent.joinpath("resources", "mpv_skip.lua"))) + args.append("--scripts-append=" + str(Path(__file__).parent.joinpath("resources", "mpv_gucken.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"] - + prepend_data = [] 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"])) - + prepend_data.append(set_default_vlc_interface_cfg("op_start", timings.op_start)) + prepend_data.append(set_default_vlc_interface_cfg("op_end", timings.op_end)) 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"])) - - prepend_data.append("-- Generated\n") + prepend_data.append(set_default_vlc_interface_cfg("ed_start", timings.ed_start)) + prepend_data.append(set_default_vlc_interface_cfg("ed_end", timings.ed_end)) 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) - vlc_skip_plugin = Path(__file__).parent.joinpath("resources", "vlc_skip.lua") - copyTo = join(vlc_intf_user_path, "vlc_skip.lua") + vlc_skip_plugin = Path(__file__).parent.joinpath("resources", "vlc_gucken.lua") + copy_to = join(vlc_intf_user_path, "vlc_gucken.lua") with open(vlc_skip_plugin, 'r') as f: original_content = f.read() - with open(copyTo, 'w') as f: + with open(copy_to, 'w') as f: f.write("\n".join(prepend_data) + original_content) - args.append("--control=luaintf{intf=vlc_skip}") + args.append("--control=luaintf{intf=vlc_gucken}") if syncplay: # TODO: make work with flatpak @@ -771,6 +759,7 @@ def delete_chapters_file(): syncplay_path, "--player-path", player_path, + # "--debug", url, "--", ] + args diff --git a/src/gucken/resources/gucken.css b/src/gucken/resources/gucken.css index 6577d07..720821d 100644 --- a/src/gucken/resources/gucken.css +++ b/src/gucken/resources/gucken.css @@ -34,7 +34,7 @@ Next > Container > Horizontal > Button { #markdown { margin: 0 0; - margin-top: -1; + margin-top: -2; } /*TODO: make height 100 of what the table needs, so there is only one scroll*/ @@ -67,4 +67,8 @@ ClickableListItem { height: 1; } +SortableTable { + width: auto; +} + /*$accent: lime;*/ diff --git a/src/gucken/resources/mpv_skip.lua b/src/gucken/resources/mpv_gucken.lua similarity index 90% rename from src/gucken/resources/mpv_skip.lua rename to src/gucken/resources/mpv_gucken.lua index 0714899..5c0997f 100644 --- a/src/gucken/resources/mpv_skip.lua +++ b/src/gucken/resources/mpv_gucken.lua @@ -2,7 +2,7 @@ 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 +if scripts_dir ~= nil and 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 diff --git a/src/gucken/resources/vlc_gucken.lua b/src/gucken/resources/vlc_gucken.lua new file mode 100644 index 0000000..a932cc3 --- /dev/null +++ b/src/gucken/resources/vlc_gucken.lua @@ -0,0 +1,57 @@ +-- 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 and ed times are given +local has_op = options.op_start and options.op_end +local has_ed = options.ed_start and options.ed_end + +while true do + local time = get_time() + + if time then + if has_op and not skipped_op and time >= options.op_start and time < options.op_end then + skipped_op = set_time(options.op_end) + end + + if has_ed and not skipped_ed and time >= options.ed_start and time < options.ed_end then + skipped_ed = set_time(options.ed_end) + end + end + + vlc.misc.mwait(vlc.misc.mdate() + 2500) -- Don't waste processor time +end diff --git a/src/gucken/resources/vlc_skip.lua b/src/gucken/resources/vlc_skip.lua deleted file mode 100755 index 9be7f20..0000000 --- a/src/gucken/resources/vlc_skip.lua +++ /dev/null @@ -1,150 +0,0 @@ --- 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/test/fuzzy.py b/test/fuzzy.py deleted file mode 100644 index c910413..0000000 --- a/test/fuzzy.py +++ /dev/null @@ -1,71 +0,0 @@ -import difflib -import logging -from difflib import SequenceMatcher - -from rome import replace_roman_numerals -from textdistance import DamerauLevenshtein - -levenshtein = DamerauLevenshtein() -import distance -from rapidfuzz import process - -logging.basicConfig(level=logging.INFO) - - -def difflib_search(pattern, possibilities, threshold=0): - matches = [] - for word in possibilities: - ratio = SequenceMatcher(None, pattern, word).ratio() - if ratio >= threshold: - matches.append((word, ratio)) - return sorted(matches, key=lambda x: x[1], reverse=True) - - -def textdistance_search(pattern, possibilities, threshold=0, limit=5): - matches = [] - for word in possibilities: - ratio = levenshtein.normalized_similarity(pattern, word) - if ratio >= threshold: - matches.append((word, ratio)) - return sorted(matches, key=lambda x: x[1], reverse=True) - - -def distance_jaccard(pattern, possibilities, threshold=0, limit=5): - matches = [] - for word in possibilities: - ratio = distance.jaccard(pattern, word) - if ratio >= threshold: - matches.append((word, ratio)) - return sorted(matches, key=lambda x: x[1]) - - -test_list = [ - "Overlord Movie 2: Shikkoku no Eiyuu", - "Overlord", - "Overlord IV", - "Overlord III", - "Overlord II", - "Overlord: Ple Ple Pleiades - Nazarick Saidai no Kiki", - "Overlord Movie 1: Fushisha no Ou", - "Overlord: Ple Ple Pleiades", - "Overlord Movie 3: Sei Oukoku-hen", - "Overlord: Ple Ple Pleiades 2", - "junk", - "over junk", - "very muxh over junk", - "very muxh junk", -] - -nl = [] -for k in test_list: - nl.append(replace_roman_numerals(k)) - -kw = replace_roman_numerals("Overlord II") - -print("Query:", kw) -print("List:", nl) -print("") -print("rapidfuzz ", process.extract(kw, nl, limit=100)) -print("difflib ", difflib_search(kw, nl)) -print("textdistance ", textdistance_search(kw, nl)) -print("jaccard ", distance_jaccard(kw, nl)) diff --git a/test/presence.py b/test/presence.py index 96df643..d249428 100644 --- a/test/presence.py +++ b/test/presence.py @@ -17,6 +17,9 @@ # small_image as playing or stopped ? small_image="https://jooinn.com/images/lonely-tree-reflection-3.jpg", small_text="ff 15", + join="R2hlbGw=", + party_id="idk", + party_size=[1, 2] # start=time.time(), # for paused # end=time.time() + timedelta(minutes=20).seconds # for time left ) # Updates our presence diff --git a/test/rome.py b/test/rome.py deleted file mode 100644 index 31733a8..0000000 --- a/test/rome.py +++ /dev/null @@ -1,24 +0,0 @@ -from re import compile as re_compile - -ROMAN_PATTERN = re_compile(r"\b[IVXLCDM]+\b") -ROMAN_NUMERALS = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} - - -def roman_to_int(roman: str) -> int: - result = 0 - prev_value = 0 - for char in reversed(roman): - value = ROMAN_NUMERALS[char] - if value < prev_value: - result -= value - else: - result += value - prev_value = value - return result - - -def replace_roman_numerals(text: str) -> str: - def repl(match): - return str(roman_to_int(match.group(0))) - - return ROMAN_PATTERN.sub(repl, text)