Skip to content

Commit be43940

Browse files
Big UI update, luluvdo support, some other small changes
1 parent 1176f96 commit be43940

File tree

15 files changed

+205
-221
lines changed

15 files changed

+205
-221
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,9 @@ List of supported video hoster.
125125
- [x] SpeedFiles
126126
- [x] Vidmoly
127127
- [x] Streamtape (Removed from AniWorld & SerienStream)
128+
- [x] Luluvdo
128129
- [ ] Filemoon
129-
- [ ] Luluvdo
130+
- [ ] LoadX
130131

131132
## Player
132133

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ maintainers = [{name="Commandcracker"}]
77
license = {file = "LICENSE.txt"}
88
readme = "README.md"
99
dependencies = [
10-
"textual==0.67.0", # 3.0.1
10+
"textual>=3.0.1",
1111
"beautifulsoup4>=4.13.3",
1212
"httpx[http2]>=0.28.1",
1313
"pypresence>=4.3.0",
1414
"packaging>=24.2",
1515
"platformdirs>=4.3.7",
1616
"toml>=0.10.2",
1717
"fuzzywuzzy>=0.18.0",
18-
"async_lru>=2.0.5"
18+
"async_lru>=2.0.5",
19+
"rich-argparse>=1.7.0"
1920
#"yt-dlp>=2025.3.31",
2021
#"mpv>=1.0.7",
2122
]

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.2.8"
4+
__version__ = "0.3.0"

src/gucken/gucken.py

Lines changed: 40 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from textual._types import IgnoreReturnCallbackType
2-
from textual.command import Hits, Provider as TextualProvider, Hit, DiscoveryHit
31
import argparse
42
import logging
53
from asyncio import gather, set_event_loop, new_event_loop
@@ -13,6 +11,7 @@
1311
from time import sleep, time
1412
from typing import ClassVar, List, Union
1513
from async_lru import alru_cache
14+
from os import getenv
1615

1716
from fuzzywuzzy import fuzz
1817
from platformdirs import user_config_path, user_log_path
@@ -41,7 +40,7 @@
4140
TabPane,
4241
)
4342
from textual.worker import get_current_worker
44-
43+
from rich_argparse import RichHelpFormatter
4544
from .aniskip import (
4645
generate_chapters_file,
4746
get_timings_from_search
@@ -58,7 +57,7 @@
5857
from .settings import gucken_settings_manager
5958
from .update import check
6059
from .utils import detect_player, is_android, set_default_vlc_interface_cfg, get_vlc_intf_user_path
61-
60+
from . import __version__
6261

6362
def sort_favorite_lang(
6463
language_list: List[Language], pio_list: List[str]
@@ -204,59 +203,17 @@ def move_item(lst: list, from_index: int, to_index: int) -> list:
204203
CLIENT_ID = "1238219157464416266"
205204

206205

207-
class GuckenCommands(TextualProvider):
208-
209-
@property
210-
def _commands(self) -> tuple[tuple[str, IgnoreReturnCallbackType, str], ...]:
211-
return (
212-
(
213-
"Toggle light/dark mode", # 🌇 / 🌃
214-
self.app.action_toggle_dark,
215-
"Toggle the application between light and dark mode",
216-
),
217-
(
218-
"Quit the application", # ❌
219-
self.app.action_quit,
220-
"Quit the application as soon as possible",
221-
),
222-
)
223-
224-
"""
225-
(
226-
"Application folders", # 📁
227-
self.app.action_quit,
228-
"Displays a list of all folders used",
229-
),
230-
(
231-
"Create Shortcut", # 🔗
232-
self.app.action_quit,
233-
"The will create shortcuts to gucken",
234-
)
235-
"""
236-
237-
async def discover(self) -> Hits:
238-
for name, runnable, help_text in self._commands:
239-
yield DiscoveryHit(name, runnable, help=help_text)
240-
241-
async def search(self, query: str) -> Hits:
242-
matcher = self.matcher(query)
243-
for name, runnable, help_text in self._commands:
244-
if (match := matcher.match(name)) > 0:
245-
yield Hit(match, matcher.highlight(name), runnable, help=help_text)
246-
247-
248206
class GuckenApp(App):
249-
TITLE = "Gucken TUI"
250-
# TODO: color theme https://textual.textualize.io/guide/design/#designing-with-colors
251-
207+
TITLE = f"Gucken {__version__}"
252208
CSS_PATH = [join("resources", "gucken.css")]
253209
custom_css = user_config_path("gucken").joinpath("custom.css")
254210
if custom_css.exists():
255211
CSS_PATH.append(custom_css)
256212
BINDINGS: ClassVar[list[BindingType]] = [
257213
Binding("q", "quit", "Quit", show=False, priority=False),
258214
]
259-
COMMANDS = {GuckenCommands}
215+
216+
# TODO: theme_changed_signal
260217

261218
def __init__(self, debug: bool, search: str):
262219
super().__init__(watch_css=debug)
@@ -322,7 +279,7 @@ def compose(self) -> ComposeResult:
322279
yield ClickableDataTable(id="season_list")
323280
with TabPane("Settings", id="setting"): # Settings "⚙"
324281
# TODO: dont show unneeded on android
325-
with ScrollableContainer():
282+
with ScrollableContainer(id="settings_container"):
326283
yield SortableTable(id="lang")
327284
yield SortableTable(id="host")
328285
yield RadioButton(
@@ -373,7 +330,7 @@ def compose(self) -> ComposeResult:
373330
)
374331
# yield Footer()
375332
with Center(id="footer"):
376-
yield Label("Made by Commandcracker with [red]:heart:[/red]")
333+
yield Label("Made by Commandcracker with [red][/red]")
377334

378335
@on(Input.Changed)
379336
async def input_changed(self, event: Input.Changed):
@@ -445,12 +402,12 @@ def select_changed(self, event: Select.Changed) -> None:
445402

446403
# TODO: dont lock - no async
447404
async def on_mount(self) -> None:
448-
self.dark = gucken_settings_manager.settings["settings"]["ui"]["dark"]
405+
self.theme = getenv("TEXTUAL_THEME") or gucken_settings_manager.settings["settings"]["ui"]["theme"]
449406

450-
def update_dark(value: bool):
451-
gucken_settings_manager.settings["settings"]["ui"]["dark"] = value
407+
def on_theme_change(old_value: str, new_value: str) -> None:
408+
gucken_settings_manager.settings["settings"]["ui"]["theme"] = new_value
452409

453-
self.watch(self, "dark", update_dark)
410+
self.watch(self.app, "theme", on_theme_change, init=False)
454411

455412
lang = self.query_one("#lang", DataTable)
456413
lang.add_columns("Language")
@@ -471,11 +428,10 @@ def set_search():
471428

472429
self.call_later(set_search)
473430

474-
self.query_one("#info", TabPane).loading = True
431+
self.query_one("#info", TabPane).set_loading(True)
475432

476433
table = self.query_one("#season_list", DataTable)
477434
table.cursor_type = "row"
478-
table.add_columns("FT", "S", "F", "Title", "Hoster", "Sprache")
479435

480436
if self.query_one("#update_checker", RadioButton).value is True:
481437
self.update_check()
@@ -534,7 +490,7 @@ def lookup_anime(self, keyword: str) -> None:
534490
if keyword is None:
535491
if not worker.is_cancelled:
536492
self.call_from_thread(results_list_view.clear)
537-
results_list_view.loading = False
493+
self.call_from_thread(results_list_view.set_loading, False)
538494
return
539495

540496
aniworld_to = self.query_one("#aniworld_to", Checkbox).value
@@ -550,7 +506,7 @@ def lookup_anime(self, keyword: str) -> None:
550506
if worker.is_cancelled:
551507
return
552508
self.call_from_thread(results_list_view.clear)
553-
results_list_view.loading = True
509+
self.call_from_thread(results_list_view.set_loading, True)
554510
if worker.is_cancelled:
555511
return
556512
results = self.sync_gather(search_providers)
@@ -577,7 +533,7 @@ def fuzzy_sort_key(result):
577533
if worker.is_cancelled:
578534
return
579535
self.call_from_thread(results_list_view.extend, items)
580-
results_list_view.loading = False
536+
self.call_from_thread(results_list_view.set_loading, False)
581537
if len(final_results) > 0:
582538

583539
def select_first_index():
@@ -621,15 +577,15 @@ async def on_key(self, event: events.Key) -> None:
621577
async def play_selected(self):
622578
dt = self.query_one("#season_list", DataTable)
623579
# TODO: show loading
624-
dt.loading = True
580+
#dt.set_loading(True)
625581
index = self.app.query_one("#results", ListView).index
626582
series_search_result = self.current[index]
627583
self.play(
628584
series_search_result=series_search_result,
629585
episodes=self.current_info.episodes,
630586
index=dt.cursor_row,
631587
)
632-
dt.loading = False
588+
#dt.set_loading(False)
633589

634590
@alru_cache(maxsize=32, ttl=600) # Cache 32 entries. Clear entry after 10 minutes.
635591
async def get_series(self, series_search_result: SearchResult):
@@ -642,7 +598,7 @@ async def open_info(self) -> None:
642598
]
643599
info_tab = self.query_one("#info", TabPane)
644600
info_tab.disabled = False
645-
info_tab.loading = True
601+
info_tab.set_loading(True)
646602
table = self.query_one("#season_list", DataTable)
647603
table.focus(scroll_visible=False)
648604
md = self.query_one("#markdown", Markdown)
@@ -651,7 +607,10 @@ async def open_info(self) -> None:
651607
self.current_info = series
652608
await md.update(series.to_markdown())
653609

654-
table.clear()
610+
# make sure to reset colum spacing
611+
table.clear(columns=True)
612+
table.add_columns("FT", "S", "F", "Title", "Hoster", "Sprache")
613+
655614
c = 0
656615
for ep in series.episodes:
657616
hl = []
@@ -671,7 +630,7 @@ async def open_info(self) -> None:
671630
" ".join(sort_favorite_hoster_by_key(hl, self.hoster)),
672631
" ".join(ll),
673632
)
674-
info_tab.loading = False
633+
info_tab.set_loading(False)
675634

676635
@work(exclusive=True, thread=True)
677636
async def update_check(self):
@@ -931,19 +890,32 @@ async def play_next(should_next):
931890

932891
def main():
933892
parser = argparse.ArgumentParser(
934-
prog='Gucken',
935-
description="Gucken is a Terminal User Interface which allows you to browse and watch your favorite anime's with style."
893+
prog='gucken',
894+
description="Gucken is a Terminal User Interface which allows you to browse and watch your favorite anime's with style.",
895+
formatter_class=RichHelpFormatter
936896
)
937897
parser.add_argument("search", nargs='?')
938-
parser.add_argument("--debug", "--dev", action="store_true")
898+
parser.add_argument(
899+
"--debug", "--dev",
900+
action="store_true",
901+
help='enables logging and live tcss reload'
902+
)
903+
parser.add_argument(
904+
'-V', '--version',
905+
action='store_true',
906+
help='display version information.'
907+
)
939908
args = parser.parse_args()
909+
if args.version:
910+
exit(f"gucken {__version__}")
940911
if args.debug:
941912
logs_path = user_log_path("gucken", ensure_exists=True)
942913
logging.basicConfig(
943914
filename=logs_path.joinpath("gucken.log"), encoding="utf-8", level=logging.INFO, force=True
944915
)
945916

946917
register_atexit(gucken_settings_manager.save)
918+
print(f"\033]0;Gucken {__version__}\007", end='', flush=True)
947919
gucken_app = GuckenApp(debug=args.debug, search=args.search)
948920
gucken_app.run()
949921
print(choice(exit_quotes))

src/gucken/hoster/_hosters.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from textual._two_way_dict import TwoWayDict
22

3+
from .loadx import LoadXHoster
34
from .veo import VOEHoster
45
from .vidoza import VidozaHoster
56
from .speedfiles import SpeedFilesHoster
@@ -17,6 +18,7 @@
1718
"DS": DoodstreamHoster,
1819
"VM": VidmolyHoster,
1920
"FM": FilemoonHoster,
21+
"LX": LoadXHoster,
2022
"LU": LuluvdoHoster,
2123
"ST": StreamtapeHoster
2224
}

src/gucken/hoster/common.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from abc import abstractmethod
22
from dataclasses import dataclass
33

4-
from ..networking import AsyncClient
5-
from httpx import HTTPError
4+
from httpx import HTTPError, AsyncClient
65

76

87
@dataclass
@@ -12,7 +11,7 @@ class DirectLink:
1211

1312
async def check_is_working(self) -> bool:
1413
try:
15-
async with AsyncClient(verify=False, auto_referer=False) as client:
14+
async with AsyncClient(verify=False) as client:
1615
response = await client.head(
1716
self.url, headers=self.headers
1817
)

src/gucken/hoster/filemoon.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
# TODO: WIP !!!
1010
class FilemoonHoster(Hoster):
1111
async def get_direct_link(self) -> DirectLink:
12+
# See https://github.com/shashstormer/godstream/blob/master/extractors/filemoon.py
1213
return DirectLink("WIP")

src/gucken/hoster/loadx.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from re import compile as re_compile
2+
3+
from ..networking import AsyncClient
4+
5+
from .common import DirectLink, Hoster
6+
7+
LOADX_PATTERN = re_compile("")
8+
9+
# TODO: WIP !!!
10+
class LoadXHoster(Hoster):
11+
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")

src/gucken/hoster/luluvdo.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
from re import compile as re_compile
22

3-
from ..networking import AsyncClient
3+
from httpx import AsyncClient
44

55
from .common import DirectLink, Hoster
66

7-
LULUVODO_PATTERN = re_compile("")
7+
LULUVODO_PATTERN = re_compile(r'file:\s*"([^"]+)"')
88

9-
# TODO: WIP !!!
109
class LuluvdoHoster(Hoster):
10+
requires_headers = True
11+
1112
async def get_direct_link(self) -> DirectLink:
12-
return DirectLink("WIP")
13+
async with AsyncClient(verify=False, follow_redirects=True, headers={"user-agent": "Mozilla/5.0 (Android 15; Mobile; rv:132.0) Gecko/132.0 Firefox/132.0"}) as client:
14+
response = await client.get(self.url)
15+
luluvdo_id = response.url.path.split('/')[-1]
16+
url = f"https://luluvdo.com/dl?op=embed&file_code={luluvdo_id}"
17+
response = await client.get(url)
18+
match = LULUVODO_PATTERN.search(response.text)
19+
return DirectLink(
20+
url=match.group(1),
21+
headers={"user-agent": client.headers["user-agent"]},
22+
)

0 commit comments

Comments
 (0)