freeflix-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. freeflix_cli/__init__.py +0 -0
  2. freeflix_cli/anilist.py +207 -0
  3. freeflix_cli/cli_utils.py +191 -0
  4. freeflix_cli/config_loader.py +62 -0
  5. freeflix_cli/defaults.py +150 -0
  6. freeflix_cli/handlers/__init__.py +0 -0
  7. freeflix_cli/handlers/anilist.py +325 -0
  8. freeflix_cli/handlers/anime_sama.py +371 -0
  9. freeflix_cli/handlers/coflix.py +308 -0
  10. freeflix_cli/handlers/french_stream.py +238 -0
  11. freeflix_cli/handlers/goldenanime.py +397 -0
  12. freeflix_cli/handlers/goldenms.py +401 -0
  13. freeflix_cli/handlers/nyaa.py +112 -0
  14. freeflix_cli/handlers/playback.py +211 -0
  15. freeflix_cli/handlers/wiflix.py +107 -0
  16. freeflix_cli/history_ui.py +95 -0
  17. freeflix_cli/i18n.py +129 -0
  18. freeflix_cli/languages.py +33 -0
  19. freeflix_cli/main.py +376 -0
  20. freeflix_cli/notifications.py +219 -0
  21. freeflix_cli/player_manager.py +799 -0
  22. freeflix_cli/providers_registry.py +50 -0
  23. freeflix_cli/proxy.py +594 -0
  24. freeflix_cli/scraping/__init__.py +0 -0
  25. freeflix_cli/scraping/anime_sama.py +291 -0
  26. freeflix_cli/scraping/coflix.py +224 -0
  27. freeflix_cli/scraping/config.py +17 -0
  28. freeflix_cli/scraping/deobfuscate.py +11 -0
  29. freeflix_cli/scraping/french_stream.py +171 -0
  30. freeflix_cli/scraping/goldenanime.py +347 -0
  31. freeflix_cli/scraping/goldenms.py +382 -0
  32. freeflix_cli/scraping/nyaa.py +89 -0
  33. freeflix_cli/scraping/objects.py +254 -0
  34. freeflix_cli/scraping/player.py +595 -0
  35. freeflix_cli/scraping/subtitles.py +127 -0
  36. freeflix_cli/scraping/utils.py +94 -0
  37. freeflix_cli/scraping/wiflix.py +176 -0
  38. freeflix_cli/subtitles.py +135 -0
  39. freeflix_cli/tracker.py +321 -0
  40. freeflix_cli/update_checker.py +66 -0
  41. freeflix_cli-1.0.0.data/data/share/freeflix-cli/data/source_portal.jsonc +7 -0
  42. freeflix_cli-1.0.0.dist-info/METADATA +261 -0
  43. freeflix_cli-1.0.0.dist-info/RECORD +46 -0
  44. freeflix_cli-1.0.0.dist-info/WHEEL +4 -0
  45. freeflix_cli-1.0.0.dist-info/entry_points.txt +2 -0
  46. freeflix_cli-1.0.0.dist-info/licenses/LICENSE +674 -0
File without changes
@@ -0,0 +1,207 @@
1
+ import json
2
+ from curl_cffi import requests
3
+ from typing import Optional, Dict, Any, List
4
+
5
+
6
+ class AniListClient:
7
+ def __init__(self, token: Optional[str] = None):
8
+ self.url = "https://graphql.anilist.co"
9
+ self.token = token
10
+ self.headers = {
11
+ "Content-Type": "application/json",
12
+ "Accept": "application/json",
13
+ }
14
+ if self.token:
15
+ self.headers["Authorization"] = f"Bearer {self.token}"
16
+
17
+ def set_token(self, token: str):
18
+ """Set the authorization token."""
19
+ self.token = token
20
+ self.headers["Authorization"] = f"Bearer {token}"
21
+
22
+ def _query(
23
+ self, query: str, variables: Dict[str, Any] = None
24
+ ) -> Optional[Dict[str, Any]]:
25
+ """Send a GraphQL query to AniList."""
26
+ try:
27
+ response = requests.post(
28
+ self.url,
29
+ json={"query": query, "variables": variables},
30
+ headers=self.headers,
31
+ timeout=10,
32
+ )
33
+ response.raise_for_status()
34
+ data = response.json()
35
+ if "errors" in data:
36
+ print(f"AniList API Error: {data['errors']}")
37
+ return None
38
+ return data["data"]
39
+ except Exception as e:
40
+ print(f"Request Error: {e}")
41
+ return None
42
+
43
+ def validate_token(self) -> Optional[Dict[str, Any]]:
44
+ """Check if the current token is valid and return user info."""
45
+ query = """
46
+ query {
47
+ Viewer {
48
+ id
49
+ name
50
+ avatar {
51
+ large
52
+ }
53
+ }
54
+ }
55
+ """
56
+ data = self._query(query)
57
+ if data and data.get("Viewer"):
58
+ return data["Viewer"]
59
+ return None
60
+
61
+ def search_media(self, search: str) -> List[Dict[str, Any]]:
62
+ """Search for an anime/manga by title."""
63
+ query = """
64
+ query ($search: String) {
65
+ Page(page: 1, perPage: 10) {
66
+ media(search: $search, type: ANIME, sort: SEARCH_MATCH) {
67
+ id
68
+ title {
69
+ romaji
70
+ english
71
+ native
72
+ }
73
+ coverImage {
74
+ medium
75
+ }
76
+ format
77
+ episodes
78
+ status
79
+ seasonYear
80
+ }
81
+ }
82
+ }
83
+ """
84
+ variables = {"search": search}
85
+ data = self._query(query, variables)
86
+ if data and data.get("Page") and data.get("Page", {}).get("media"):
87
+ return data["Page"]["media"]
88
+ return []
89
+
90
+ def get_media_status(self, media_id: int, user_id: int) -> Optional[Dict[str, Any]]:
91
+ """Get the user's current status for a specific media."""
92
+ query = """
93
+ query ($mediaId: Int, $userId: Int) {
94
+ MediaList(mediaId: $mediaId, userId: $userId) {
95
+ id
96
+ status
97
+ progress
98
+ score
99
+ }
100
+ }
101
+ """
102
+ variables = {"mediaId": media_id, "userId": user_id}
103
+ # Note: MediaList returns 404/null if not found, we need to handle that gracefully
104
+ try:
105
+ data = self._query(
106
+ query, variables
107
+ ) # This might fail if not found depending on API behavior
108
+ if data:
109
+ return data.get("MediaList")
110
+ except Exception:
111
+ pass
112
+ return None
113
+
114
+ def update_progress(self, media_id: int, progress: int) -> bool:
115
+ """Update the progress for a media item. Also sets status to CURRENT if not already."""
116
+ query = """
117
+ mutation ($mediaId: Int, $progress: Int, $status: MediaListStatus) {
118
+ SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status) {
119
+ id
120
+ progress
121
+ status
122
+ }
123
+ }
124
+ """
125
+ # We enforce status to CURRENT whenever we update progress,
126
+ # unless it's already completed (logic could be refined)
127
+ variables = {"mediaId": media_id, "progress": progress, "status": "CURRENT"}
128
+ data = self._query(query, variables)
129
+ return data is not None and "SaveMediaListEntry" in data
130
+
131
+ def get_user_watching(self, user_id: int) -> List[Dict[str, Any]]:
132
+ """Get the user's current watching list."""
133
+ query = """
134
+ query ($userId: Int) {
135
+ MediaListCollection(userId: $userId, type: ANIME, status: CURRENT, sort: UPDATED_TIME_DESC) {
136
+ lists {
137
+ entries {
138
+ mediaId
139
+ progress
140
+ media {
141
+ title {
142
+ romaji
143
+ english
144
+ }
145
+ coverImage {
146
+ medium
147
+ }
148
+ format
149
+ episodes
150
+ seasonYear
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ """
157
+ variables = {"userId": user_id}
158
+ data = self._query(query, variables)
159
+ if (
160
+ data
161
+ and data.get("MediaListCollection")
162
+ and data["MediaListCollection"].get("lists")
163
+ ):
164
+ # Flatten lists (there should be only one for CURRENT usually, but structure is a list of lists)
165
+ entries = []
166
+ for lst in data["MediaListCollection"]["lists"]:
167
+ entries.extend(lst.get("entries", []))
168
+ return entries
169
+ return []
170
+
171
+ def get_media_with_relations(self, media_id: int) -> Optional[Dict[str, Any]]:
172
+ """Get media details including episode count and relations (sequels)."""
173
+ query = """
174
+ query ($mediaId: Int) {
175
+ Media(id: $mediaId) {
176
+ id
177
+ title {
178
+ romaji
179
+ english
180
+ }
181
+ episodes
182
+ relations {
183
+ edges {
184
+ relationType
185
+ node {
186
+ id
187
+ title {
188
+ romaji
189
+ english
190
+ }
191
+ episodes
192
+ format
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ """
199
+ variables = {"mediaId": media_id}
200
+ data = self._query(query, variables)
201
+ if data and data.get("Media"):
202
+ return data["Media"]
203
+ return None
204
+
205
+
206
+ # Global instance
207
+ anilist_client = AniListClient()
@@ -0,0 +1,191 @@
1
+ import os
2
+ import readchar
3
+ import re
4
+ from rich.console import Console, Group
5
+ from rich.panel import Panel
6
+ from rich.text import Text
7
+ from rich.live import Live
8
+ from rich import print as rprint
9
+
10
+ console = Console()
11
+
12
+
13
+ def clear_screen():
14
+ """Clear the terminal screen."""
15
+ os.system("cls" if os.name == "nt" else "clear")
16
+
17
+
18
+ def get_user_input(prompt: str, default: str = None) -> str:
19
+ """
20
+ Get user input with a styled prompt.
21
+
22
+ Args:
23
+ prompt: The prompt text to display
24
+ default: The default value to display
25
+
26
+ Returns:
27
+ The user's input as a string
28
+ """
29
+ styled_prompt = Text(f"\n❯ {prompt}: ", style="bold cyan")
30
+ console.print(styled_prompt, end="")
31
+ return input().strip() or default
32
+
33
+
34
+ def pause():
35
+ """Wait for user input before continuing."""
36
+ console.input("\n[dim]Press Enter to continue...[/dim]")
37
+
38
+
39
+ def select_from_list(options: list[str], prompt: str, default_index: int = 0) -> int:
40
+ """
41
+ Display an interactive menu where users can navigate with arrow keys.
42
+
43
+ Args:
44
+ options: List of options to display
45
+ prompt: Header text for the menu
46
+ default_index: Index to select by default
47
+
48
+ Returns:
49
+ Index of the selected option (0-based)
50
+ """
51
+ selected_index = max(0, min(default_index, len(options) - 1))
52
+ start_index = 0
53
+
54
+ def generate_renderable():
55
+ nonlocal start_index
56
+
57
+ # Calculate dynamic window size based on terminal height
58
+ term_height = console.size.height
59
+ # Reserve lines for prompt (2), header/spacing (2), arrows (2) -> ~6 lines reserve
60
+ reserved_lines = 6
61
+ available_height = max(3, term_height - reserved_lines)
62
+ window_size = min(len(options), available_height)
63
+
64
+ # Adjust start_index to keep selected_index in view
65
+ if selected_index < start_index:
66
+ start_index = selected_index
67
+ elif selected_index >= start_index + window_size:
68
+ start_index = selected_index - window_size + 1
69
+
70
+ # Ensure start_index is valid
71
+ start_index = max(0, min(start_index, len(options) - window_size))
72
+ end_index = min(len(options), start_index + window_size)
73
+
74
+ lines = [Text(f"\n❯ {prompt}", style="bold cyan")]
75
+
76
+ # Up arrow indicator
77
+ if start_index > 0:
78
+ lines.append(Text(" ↑ ...", style="dim"))
79
+
80
+ for idx in range(start_index, end_index):
81
+ option = options[idx]
82
+ if idx == selected_index:
83
+ lines.append(Text(f" ● {option}", style="green bold"))
84
+ else:
85
+ lines.append(Text(f" {option}", style="white"))
86
+
87
+ # Down arrow indicator
88
+ if end_index < len(options):
89
+ lines.append(Text(" ↓ ...", style="dim"))
90
+
91
+ return Group(*lines)
92
+
93
+ with Live(generate_renderable(), refresh_per_second=10, transient=True) as live:
94
+ while True:
95
+ key = readchar.readkey()
96
+
97
+ if key == readchar.key.UP:
98
+ selected_index = (selected_index - 1) % len(options)
99
+ live.update(generate_renderable())
100
+ elif key == readchar.key.DOWN:
101
+ selected_index = (selected_index + 1) % len(options)
102
+ live.update(generate_renderable())
103
+ elif key == readchar.key.ENTER:
104
+ break
105
+ elif key == readchar.key.CTRL_C:
106
+ raise KeyboardInterrupt("Menu cancelled by user")
107
+
108
+ console.print(
109
+ f"\n[bold cyan]❯ {prompt}[/bold cyan] [green]{options[selected_index]}[/green]"
110
+ )
111
+ return selected_index
112
+
113
+
114
+ def print_header(text: str):
115
+ """
116
+ Print a styled header with a decorative panel.
117
+
118
+ Args:
119
+ text: Header text to display
120
+ """
121
+ console.print()
122
+ panel = Panel(
123
+ Text(text, style="bold white", justify="center"),
124
+ style="cyan",
125
+ border_style="bright_cyan",
126
+ padding=(0, 2),
127
+ )
128
+ console.print(panel)
129
+
130
+
131
+ def print_success(message: str):
132
+ """
133
+ Print a success message with a green checkmark.
134
+
135
+ Args:
136
+ message: Success message to display
137
+ """
138
+ console.print(f"[green]✓[/green] {message}")
139
+
140
+
141
+ def print_error(message: str):
142
+ """
143
+ Print an error message with a red X.
144
+
145
+ Args:
146
+ message: Error message to display
147
+ """
148
+ console.print(f"[red]✗[/red] {message}")
149
+
150
+
151
+ def print_info(message: str):
152
+ """
153
+ Print an info message with a blue icon.
154
+
155
+ Args:
156
+ message: Info message to display
157
+ """
158
+ console.print(f"[blue]ℹ[/blue] {message}")
159
+
160
+
161
+ def print_warning(message: str):
162
+ """
163
+ Print a warning message with a yellow icon.
164
+
165
+ Args:
166
+ message: Warning message to display
167
+ """
168
+ console.print(f"[yellow]⚠[/yellow] {message}")
169
+
170
+
171
+ def clean_title(title: str) -> str:
172
+ """
173
+ Remove season and part indicators from a title to help with search.
174
+ Example: "One Piece Season 4" -> "One Piece"
175
+ """
176
+ # Remove Season X, S2, Part 2, etc. (case insensitive)
177
+ # Common patterns: Season 1, S1, Part 1, Cour 1, etc.
178
+ patterns = [
179
+ r"\s+Season\s+\d+",
180
+ r"\s+S\d+",
181
+ r"\s+Part\s+\d+",
182
+ r"\s+Cour\s+\d+",
183
+ r"\s+\d+(st|nd|rd|th)\s+Season",
184
+ r"\s+-\s+\d+", # Sometimes title - 2
185
+ ]
186
+
187
+ cleaned = title
188
+ for pattern in patterns:
189
+ cleaned = re.sub(pattern, "", cleaned, flags=re.IGNORECASE)
190
+
191
+ return cleaned.strip()
@@ -0,0 +1,62 @@
1
+ import json
2
+ import re
3
+ from curl_cffi import requests
4
+
5
+
6
+ def strip_json_comments(json_str: str) -> str:
7
+ """
8
+ Remove // and /* */ comments from JSON string.
9
+ """
10
+ # Remove // comments
11
+ json_str = re.sub(r"(?<!:)\/\/.*$", "", json_str, flags=re.MULTILINE)
12
+ # Remove /* */ comments
13
+ json_str = re.sub(r"\/\*.*?\*\/", "", json_str, flags=re.DOTALL)
14
+ return json_str
15
+
16
+
17
+ def strip_trailing_commas(json_str: str) -> str:
18
+ """
19
+ Remove trailing commas from JSON objects and arrays.
20
+ """
21
+ return re.sub(r",\s*([\]}])", r"\1", json_str)
22
+
23
+
24
+ def load_remote_jsonc(url: str, default: dict) -> dict:
25
+ """
26
+ Fetch a remote JSONC file, strip comments, and parse it.
27
+ Returns the default dictionary if fetching or parsing fails.
28
+ """
29
+ try:
30
+ response = requests.get(url, impersonate="chrome", timeout=5)
31
+ response.raise_for_status()
32
+
33
+ clean_json = strip_json_comments(response.text)
34
+ clean_json = strip_trailing_commas(clean_json)
35
+
36
+ return json.loads(clean_json)
37
+ except Exception as e:
38
+ print(f"Warning: Failed to load remote config from {url}: {e}")
39
+ return default
40
+
41
+
42
+ def load_local_jsonc(file_path: str, default: dict = None) -> dict:
43
+ """
44
+ Load a local JSONC file, strip comments, and parse it.
45
+ Returns default if the file is missing or invalid.
46
+ """
47
+ import os
48
+
49
+ if not os.path.exists(file_path):
50
+ return default or {}
51
+
52
+ try:
53
+ with open(file_path, "r", encoding="utf-8") as f:
54
+ content = f.read()
55
+
56
+ clean_json = strip_json_comments(content)
57
+ clean_json = strip_trailing_commas(clean_json)
58
+
59
+ return json.loads(clean_json)
60
+ except Exception as e:
61
+ print(f"Warning: Failed to load local config from {file_path}: {e}")
62
+ return default or {}
@@ -0,0 +1,150 @@
1
+ # Default configurations acting as fallback
2
+ # These values are used if remote configuration cannot be loaded
3
+
4
+ DEFAULT_PLAYERS = {
5
+ "wishonly": {
6
+ "type": "default",
7
+ "referrer": "full",
8
+ "alt-used": True,
9
+ "sec_headers": "Sec-Fetch-Dest:empty;Sec-Fetch-Mode:cors;Sec-Fetch-Site:cross-site;Content-Cache: no-cache",
10
+ "mode": "proxy",
11
+ },
12
+ "hgbazooka": {"type": "default"},
13
+ "hailindihg": {"type": "default"},
14
+ "gradehgplus": {"type": "default"},
15
+ "taylorplayer": {"type": "default"},
16
+ "vidmoly": {"type": "vidmoly"},
17
+ # "oneupload": {"type": "default"},
18
+ "tipfly": {"type": "default"},
19
+ # "luluvdoo": {
20
+ # "type": "b",
21
+ # "sec_headers": "Sec-Fetch-Dest:empty;Sec-Fetch-Mode:cors;Sec-Fetch-Site:cross-site",
22
+ # },
23
+ # "luluvdo": {
24
+ # "type": "b",
25
+ # "sec_headers": False,
26
+ # },
27
+ # "lulustream": {
28
+ # "type": "b",
29
+ # "sec_headers": "Sec-Fetch-Dest:empty;Sec-Fetch-Mode:cors;Sec-Fetch-Site:cross-site",
30
+ # },
31
+ "ups2up": {"type": "default"},
32
+ "ico3c": {"type": "default"},
33
+ "fsvid": {"type": "default", "m3u8-extractor": {"no-header": True}},
34
+ "darkibox": {"type": "default"},
35
+ "minochinos": {"type": "default"},
36
+ "movearnpre": {
37
+ "type": "default",
38
+ "referrer": "full",
39
+ "alt-used": False,
40
+ "sec_headers": "Sec-Fetch-Dest:empty;Sec-Fetch-Mode:cors;Sec-Fetch-Site:same-origin",
41
+ },
42
+ "smoothpre": {
43
+ "type": "default",
44
+ "referrer": "full",
45
+ "alt-used": True,
46
+ "sec_headers": "Sec-Fetch-Dest:empty;Sec-Fetch-Mode:cors;Sec-Fetch-Site:cross-site;Content-Cache: no-cache",
47
+ "mode": "proxy",
48
+ },
49
+ "vidhideplus": {"type": "default"},
50
+ "dinisglows": {
51
+ "type": "default",
52
+ "referrer": "full",
53
+ "alt-used": True,
54
+ "sec_headers": "Sec-Fetch-Dest:empty;Sec-Fetch-Mode:cors;Sec-Fetch-Site:same-origin",
55
+ },
56
+ "mivalyo": {"type": "default"},
57
+ "dingtezuni": {"type": "default"},
58
+ "bingezove": {"type": "default"},
59
+ "vidzy": {"type": "default"},
60
+ "ok.ru": {"type": "default"},
61
+ "videzz": {
62
+ "type": "vidoza",
63
+ "mode": "proxy",
64
+ "no-header": True,
65
+ "ext": "mp4",
66
+ },
67
+ "vidoza": {
68
+ "type": "vidoza",
69
+ "mode": "proxy",
70
+ "no-header": True,
71
+ "ext": "mp4",
72
+ },
73
+ "sendvid": {"type": "sendvid", "mode": "proxy", "ext": "mp4"},
74
+ "sibnet": {
75
+ "type": "sibnet",
76
+ "mode": "proxy",
77
+ "ext": "mp4",
78
+ "referrer": "full",
79
+ "no-header": True,
80
+ },
81
+ "uqload": {
82
+ "type": "uqload",
83
+ "sec_headers": "Sec-Fetch-Dest:video;Sec-Fetch-Mode:no-cors;Sec-Fetch-Site:same-site",
84
+ "ext": "mp4",
85
+ },
86
+ "filemoon": {
87
+ "type": "filemoon",
88
+ "referrer": "https://ico3c.com/",
89
+ "no-header": True,
90
+ },
91
+ "bysebuho": {
92
+ "type": "filemoon",
93
+ "referrer": "https://ico3c.com/",
94
+ "no-header": True,
95
+ },
96
+ "bysekoze": {
97
+ "type": "filemoon",
98
+ "referrer": "https://ico3c.com/",
99
+ "no-header": True,
100
+ },
101
+ "kakaflix": {"type": "kakaflix"},
102
+ # "myvidplay": {"type": "myvidplay", "referrer": "https://myvidplay.com/"},
103
+ "embed4me": {"type": "embed4me"},
104
+ "coflix.upn": {"type": "embed4me"},
105
+ "veev": {"type": "veev", "ext": "mp4"},
106
+ "xtremestream": {"type": "xtremestream"},
107
+ }
108
+
109
+ DEFAULT_NEW_URL = {
110
+ "lulustream": "luluvdo",
111
+ "vidoza.net": "videzz.net",
112
+ "oneupload.to": "oneupload.net",
113
+ "uqload.cx": "uqload.is",
114
+ # Dinisglows Player
115
+ "mivalyo": "dinisglows",
116
+ "vidhideplus": "dinisglows",
117
+ "dingtezuni": "dinisglows",
118
+ # Vidmoly Player
119
+ "vidmoly.to": "vidmoly.biz",
120
+ "vidmoly.me": "vidmoly.biz",
121
+ "vidmoly.net": "vidmoly.biz",
122
+ }
123
+
124
+ DEFAULT_KAKAFLIX_PLAYERS = {
125
+ "moon2": "ico3c",
126
+ "viper": "ico3c",
127
+ # "tokyo": "myvidplay"
128
+ }
129
+
130
+ DEFAULT_SOURCE_PORTAL = {
131
+ "french-stream": "https://french-stream.one",
132
+ "anime-sama": "https://animes-sama.fr",
133
+ "coflix": "https://coflix.fans",
134
+ "sudatchi": "https://sudatchi.com",
135
+ "animetsu": "https://animetsu.live",
136
+ "animetsu-api": "https://b.animetsu.live",
137
+ "animetsu-proxy": "https://ani.metsu.site/proxy",
138
+ "allanime-api": "https://api.allanime.day/api",
139
+ "allanime-referer": "https://allmanga.to",
140
+ "allanime-base": "https://allanime.day",
141
+ "anizone": "https://anizone.to",
142
+ "videasy": "https://api.videasy.net",
143
+ "videasy-referer": "https://cineby.gd",
144
+ "vidlink": "https://vidlink.pro",
145
+ "hexa": "https://themoviedb.hexa.su",
146
+ "hexa-referer": "https://hexa.su",
147
+ "multi-decrypt": "https://enc-dec.app/api",
148
+ "mapple": "https://mapple.uk",
149
+ "xpass": "https://play.xpass.top",
150
+ }
File without changes