Skip to content

Commit 9de674c

Browse files
Add LoadX, Fix VEO, Add PiP mode
1 parent f3393e0 commit 9de674c

File tree

11 files changed

+132
-40
lines changed

11 files changed

+132
-40
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ List of supported video hoster.
126126
- [x] Vidmoly
127127
- [x] Streamtape (Removed from AniWorld & SerienStream)
128128
- [x] Luluvdo
129+
- [x] LoadX
129130
- [ ] Filemoon
130-
- [ ] LoadX
131131

132132
## Player
133133

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ maintainers = [{name="Commandcracker"}]
77
license = {file = "LICENSE.txt"}
88
readme = "README.md"
99
dependencies = [
10-
"textual>=3.0.1",
11-
"beautifulsoup4>=4.13.3",
10+
"textual>=3.1.1",
11+
"beautifulsoup4>=4.13.4",
1212
"httpx[http2]>=0.28.1",
1313
"pypresence>=4.3.0",
14-
"packaging>=24.2",
14+
"packaging>=25.0",
1515
"platformdirs>=4.3.7",
1616
"toml>=0.10.2",
1717
"fuzzywuzzy>=0.18.0",
1818
"async_lru>=2.0.5",
1919
"rich-argparse>=1.7.0"
2020
#"yt-dlp>=2025.3.31",
21-
#"mpv>=1.0.7",
21+
#"mpv>=1.0.8",
2222
]
2323
keywords = [
2424
"gucken",

src/gucken/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import warnings
22
warnings.filterwarnings('ignore', message='Using slow pure-python SequenceMatcher. Install python-Levenshtein to remove this warning')
33

4-
__version__ = "0.3.2"
4+
__version__ = "0.3.3"

src/gucken/gucken.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,11 @@ def compose(self) -> ComposeResult:
304304
id="autoplay",
305305
value=settings["autoplay"]["enabled"],
306306
)
307+
yield RadioButton(
308+
"PiP Mode (MPV & VLC only)",
309+
id="pip",
310+
value=settings["pip"],
311+
)
307312
yield Select.from_values(
308313
available_players_keys,
309314
id="player",
@@ -312,7 +317,7 @@ def compose(self) -> ComposeResult:
312317
Select.BLANK if player == "AutomaticPlayer" else player
313318
),
314319
)
315-
with Collapsible(title="ani-skip (only for MPV and VLC)", collapsed=False):
320+
with Collapsible(title="ani-skip (MPV & VLC only)", collapsed=False):
316321
yield RadioButton(
317322
"Skip opening",
318323
id="ani_skip_opening",
@@ -381,6 +386,10 @@ async def radio_button_changed(self, event: RadioButton.Changed):
381386
settings["autoplay"]["enabled"] = event.value
382387
return
383388

389+
if id == "pip":
390+
settings["pip"] = event.value
391+
return
392+
384393
settings[id] = event.value
385394

386395
if id == "discord_presence":
@@ -707,6 +716,24 @@ async def update():
707716

708717
self.app.call_later(update)
709718

719+
# Picture-in-Picture mode
720+
if gucken_settings_manager.settings["settings"]["pip"]:
721+
if isinstance(_player, MPVPlayer):
722+
args.append("--ontop")
723+
args.append("--no-border")
724+
args.append("--snap-window")
725+
726+
if isinstance(_player, VLCPlayer):
727+
args.append("--video-on-top")
728+
args.append("--qt-minimal-view")
729+
args.append("--no-video-deco")
730+
731+
if direct_link.force_hls:
732+
# TODO: make work for vlc and others
733+
if isinstance(_player, MPVPlayer):
734+
args.append("--demuxer=lavf")
735+
args.append("--demuxer-lavf-format=hls")
736+
710737
if self._debug:
711738
logs_path = user_log_path("gucken", ensure_exists=True)
712739
if isinstance(_player, MPVPlayer):

src/gucken/hoster/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
class DirectLink:
99
url: str
1010
headers: dict[str, str] = None
11+
force_hls: bool = False
1112

1213
async def check_is_working(self) -> bool:
1314
try:

src/gucken/hoster/loadx.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
from re import compile as re_compile
2-
31
from ..networking import AsyncClient
4-
52
from .common import DirectLink, Hoster
3+
from ..utils import json_loads
64

7-
LOADX_PATTERN = re_compile("")
85

9-
# TODO: WIP !!!
106
class LoadXHoster(Hoster):
117
async def get_direct_link(self) -> DirectLink:
12-
# https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/loadx.py
13-
# https://github.com/bytedream/stream-bypass/blob/c4085f9ac83d9313ebc8e9629067c91dc7fbe064/src/lib/match.ts#L122
14-
15-
return DirectLink("WIP")
8+
async with AsyncClient(verify=False) as client:
9+
response = await client.head(self.url)
10+
id_hash = response.url.path.split("/")[2]
11+
host = response.url.host
12+
response = await client.post(
13+
f"https://{host}/player/index.php?data={id_hash}&do=getVideo",
14+
headers={"X-Requested-With": "XMLHttpRequest"}
15+
)
16+
data = json_loads(response.text)
17+
return DirectLink(url=data.get("videoSource"), force_hls=True)

src/gucken/hoster/veo.py

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,81 @@
11
from base64 import b64decode
2-
from re import compile as re_compile
3-
from ..utils import json_loads
2+
from re import compile as re_compile, escape as re_escape
3+
4+
from bs4 import BeautifulSoup
45

6+
from ..utils import json_loads
57
from ..networking import AsyncClient
68
from .common import DirectLink, Hoster
79

810
REDIRECT_PATTERN = re_compile("https?://[^'\"<>]+")
911

10-
EXTRACT_VEO_HLS_PATTERN = re_compile(r"'hls': '(?P<hls>.*)'")
11-
HIDDEN_JSON_PATTERN = re_compile(r"var a168c='(?P<hidden_json>[^']+)'")
12+
# Credit
13+
# https://github.com/wolfswolke/aniworld_scraper/blob/41bd0f23cbc02352481dd92e6d986d1fe30c76bf/src/logic/search_for_links.py#L23
14+
15+
def deb_func1(input_string):
16+
result = ''
17+
for char in input_string:
18+
char_code = ord(char)
19+
if 0x41 <= char_code <= 0x5a:
20+
char_code = (char_code - 0x41 + 0xd) % 0x1a + 0x41
21+
elif 0x61 <= char_code <= 0x7a:
22+
char_code = (char_code - 0x61 + 0xd) % 0x1a + 0x61
23+
result += chr(char_code)
24+
return result
25+
26+
PATTERNS = [
27+
re_compile(re_escape('@$')),
28+
re_compile(re_escape('^^')),
29+
re_compile(re_escape('~@')),
30+
re_compile(re_escape('%?')),
31+
re_compile(re_escape('*~')),
32+
re_compile(re_escape('!!')),
33+
re_compile(re_escape('#&'))
34+
]
35+
36+
def regex_func(input_string):
37+
for pattern in PATTERNS:
38+
input_string = pattern.sub('_', input_string)
39+
return input_string
40+
41+
def deb_func3(input_string, shift):
42+
result = []
43+
for char in input_string:
44+
result.append(chr(ord(char) - shift))
45+
return ''.join(result)
46+
47+
def deb_func(input_var):
48+
math_output = deb_func1(input_var)
49+
regexed_string = regex_func(math_output)
50+
cleaned_string = regexed_string.replace('_', '')
51+
b64_string1 = b64decode(cleaned_string).decode('utf-8')
52+
decoded_string = deb_func3(b64_string1, 3)
53+
reversed_string = decoded_string[::-1]
54+
b64_string2 = b64decode(reversed_string).decode('utf-8')
55+
return json_loads(b64_string2)
56+
57+
def find_script_element(raw_html):
58+
soup = BeautifulSoup(raw_html, features="html.parser")
59+
script_object = soup.find_all("script")
60+
obfuscated_string = ""
61+
for script in script_object:
62+
script = str(script)
63+
if "KGMAaM=" in script:
64+
obfuscated_string = script
65+
break
66+
if obfuscated_string == "":
67+
return None
68+
obfuscated_string = obfuscated_string.split('MKGMa="')[1]
69+
obfuscated_string = obfuscated_string.split('"')[0]
70+
output = deb_func(obfuscated_string)
71+
return output["source"]
72+
1273

1374
class VOEHoster(Hoster):
1475
async def get_direct_link(self) -> DirectLink:
1576
async with AsyncClient(verify=False) as client:
1677
redirect_response = await client.get(self.url)
1778
redirect_match = REDIRECT_PATTERN.search(redirect_response.text)
1879
redirect_link = redirect_match.group()
19-
2080
response = await client.get(redirect_link)
21-
22-
match = HIDDEN_JSON_PATTERN.search(response.text)
23-
if match:
24-
hidden_json = b64decode(match.group("hidden_json")).decode()
25-
hidden_json = hidden_json[::-1]
26-
hidden_json = json_loads(hidden_json)
27-
hidden_json = hidden_json["source"]
28-
return DirectLink(hidden_json)
29-
30-
hls_match = EXTRACT_VEO_HLS_PATTERN.search(response.text)
31-
hls_link = hls_match.group("hls")
32-
hls_link = b64decode(hls_link).decode()
33-
return DirectLink(
34-
url=hls_link,
35-
# Requires "host", "origin" or "referer"
36-
# can be "bypassed" by http get once for players without headers
37-
headers = {"Referer": "https://nathanfromsubject.com/"}
38-
)
81+
return DirectLink(find_script_element(response.text))

src/gucken/player/vlc.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,17 @@ def play(
3737
if title:
3838
args.append(f"--input-title-format={title}")
3939
if headers:
40+
# Hehe
4041
if headers.get("Referer"):
4142
args.append(f"--http-referrer={headers.get('Referer')}")
43+
if headers.get("referer"):
44+
args.append(f"--http-referrer={headers.get('referer')}")
4245
if headers.get("User-Agent"):
4346
args.append(f"--http-user-agent={headers.get('User-Agent')}")
47+
if headers.get("user-agent"):
48+
args.append(f"--http-user-agent={headers.get('user-agent')}")
49+
if headers.get("User-agent"):
50+
args.append(f"--http-user-agent={headers.get('User-agent')}")
51+
if headers.get("user-Agent"):
52+
args.append(f"--http-user-agent={headers.get('user-Agent')}")
4453
return args

src/gucken/provider/aniworld.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ..hoster.filemoon import FilemoonHoster
1414
from ..hoster.luluvdo import LuluvdoHoster
1515
from ..hoster.streamtape import StreamtapeHoster
16+
from ..hoster.loadx import LoadXHoster
1617
from .common import Episode, Hoster, Language, Provider, SearchResult, Series
1718
from ..utils import json_loads
1819
from ..utils import fully_unescape
@@ -34,6 +35,8 @@ def provider_to_hoster(provider: str, url: str) -> Hoster:
3435
return LuluvdoHoster(url)
3536
if provider == "Vidmoly":
3637
return VidmolyHoster(url)
38+
if provider == "LoadX":
39+
return LoadXHoster(url)
3740

3841

3942
def lang_img_src_lang_name_to_lang(name: str) -> Language:
@@ -295,6 +298,8 @@ async def get_episodes_from_soup(
295298
hoster.add(LuluvdoHoster)
296299
if t == "Vidmoly":
297300
hoster.add(VidmolyHoster)
301+
if t == "LoadX":
302+
hoster.add(LoadXHoster)
298303

299304
e_count += 1
300305
title_en = fully_unescape(title.find("span").text.strip())

src/gucken/provider/serienstream.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from bs4 import BeautifulSoup
66

7+
from ..hoster.loadx import LoadXHoster
78
from ..networking import AcceptLanguage, AsyncClient
89
from ..hoster.veo import VOEHoster
910
from ..hoster.vidoza import VidozaHoster
@@ -44,7 +45,8 @@ def provider_to_hoster(provider: str, url: str) -> Hoster:
4445
return LuluvdoHoster(url)
4546
if provider == "Vidmoly":
4647
return VidmolyHoster(url)
47-
48+
if provider == "LoadX":
49+
return LoadXHoster(url)
4850

4951
def lang_img_src_lang_name_to_lang(name: str) -> Language:
5052
if name == "english":
@@ -306,6 +308,8 @@ async def get_episodes_from_soup(
306308
hoster.add(LuluvdoHoster)
307309
if t == "Vidmoly":
308310
hoster.add(VidmolyHoster)
311+
if t == "LoadX":
312+
hoster.add(LoadXHoster)
309313

310314
e_count += 1
311315
title_en = fully_unescape(title.find("span").text.strip())

src/gucken/resources/default_settings.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Default settings
22
[settings]
3+
pip = false
34
fullscreen = true
45
syncplay = false
56
discord_presence = false

0 commit comments

Comments
 (0)