flix-cli 1.6.1__py3-none-any.whl → 1.6.4__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.
- flix_cli/core/__flix_cli__.py +421 -201
- flix_cli/core/__version__.py +1 -1
- flix_cli/core/utils/__player__.py +3 -3
- flix_cli-1.6.4.dist-info/METADATA +22 -0
- flix_cli-1.6.4.dist-info/RECORD +12 -0
- {flix_cli-1.6.1.dist-info → flix_cli-1.6.4.dist-info}/WHEEL +1 -1
- flix_cli-1.6.1.dist-info/METADATA +0 -143
- flix_cli-1.6.1.dist-info/RECORD +0 -12
- {flix_cli-1.6.1.dist-info → flix_cli-1.6.4.dist-info}/LICENSE +0 -0
- {flix_cli-1.6.1.dist-info → flix_cli-1.6.4.dist-info}/entry_points.txt +0 -0
flix_cli/core/__flix_cli__.py
CHANGED
@@ -1,262 +1,474 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
1
3
|
import httpx
|
2
|
-
|
3
|
-
import re
|
4
|
+
import regex as re
|
4
5
|
from fzf import fzf_prompt
|
5
|
-
|
6
|
-
import base64
|
7
6
|
import subprocess
|
8
7
|
import platform
|
9
8
|
import os
|
10
9
|
|
11
10
|
from .utils.__player__ import play
|
12
11
|
from .utils.__downloader__ import download
|
12
|
+
from .__version__ import __core__
|
13
13
|
|
14
14
|
try:
|
15
15
|
import orjson as json
|
16
16
|
except ImportError:
|
17
17
|
import json
|
18
18
|
|
19
|
-
|
20
|
-
#from colorama import Fore, Style
|
21
19
|
import sys
|
20
|
+
from urllib.parse import urljoin, quote
|
21
|
+
import time
|
22
|
+
from bs4 import BeautifulSoup
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
def aes_encrypt(data: str, *, key, iv):
|
29
|
-
return base64.b64encode(
|
30
|
-
AES.new(key, AES.MODE_CBC, iv=iv).encrypt(pad(data).encode())
|
31
|
-
)
|
32
|
-
|
33
|
-
|
34
|
-
def aes_decrypt(data: str, *, key, iv):
|
35
|
-
return (
|
36
|
-
AES.new(key, AES.MODE_CBC, iv=iv)
|
37
|
-
.decrypt(base64.b64decode(data))
|
38
|
-
.strip(b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10")
|
39
|
-
)
|
40
|
-
|
41
|
-
|
42
|
-
headers = {"User-Agent": "flix-cli/1.5.7"}
|
24
|
+
headers = {
|
25
|
+
"User-Agent": f"flix-cli/{__core__}",
|
26
|
+
"Referer": "https://flixhq.to/",
|
27
|
+
"X-Requested-With": "XMLHttpRequest"
|
28
|
+
}
|
43
29
|
|
44
30
|
client = httpx.Client(headers=headers, follow_redirects=True, timeout=None)
|
45
|
-
#cyan = lambda a: f"{Fore.CYAN}{a}{Style.RESET_ALL}"
|
46
31
|
|
47
|
-
|
48
|
-
|
32
|
+
FLIXHQ_BASE_URL = "https://flixhq.to"
|
33
|
+
FLIXHQ_SEARCH_URL = f"{FLIXHQ_BASE_URL}/search"
|
34
|
+
FLIXHQ_AJAX_URL = f"{FLIXHQ_BASE_URL}/ajax"
|
35
|
+
DECODER = "https://dec.eatmynerds.live"
|
49
36
|
|
50
|
-
|
37
|
+
selected_media = None
|
38
|
+
selected_subtitles = []
|
51
39
|
|
52
|
-
|
40
|
+
def decode_url(url: str):
|
41
|
+
"""Decode the stream URL using the decoding service"""
|
42
|
+
try:
|
43
|
+
print(f"Debug: Decoding URL: {url}")
|
44
|
+
decoder_endpoint = f"{DECODER}?url={quote(url)}"
|
45
|
+
|
46
|
+
resp = client.get(decoder_endpoint, headers={
|
47
|
+
"User-Agent": headers["User-Agent"],
|
48
|
+
"Referer": FLIXHQ_BASE_URL
|
49
|
+
})
|
50
|
+
|
51
|
+
if resp.status_code == 200:
|
52
|
+
try:
|
53
|
+
data = resp.json()
|
54
|
+
if 'sources' in data and data['sources']:
|
55
|
+
video_link = data['sources'][0].get('file', '')
|
56
|
+
if video_link and '.m3u8' in video_link:
|
57
|
+
print(f"Debug: Found m3u8 URL: {video_link}")
|
58
|
+
subtitles = []
|
59
|
+
if 'tracks' in data:
|
60
|
+
for track in data['tracks']:
|
61
|
+
if track.get('kind') == 'captions' and track.get('file'):
|
62
|
+
subtitles.append(track['file'])
|
63
|
+
return video_link, subtitles
|
64
|
+
|
65
|
+
# Try other common fields
|
66
|
+
for key in ['link', 'url', 'file']:
|
67
|
+
if key in data and data[key]:
|
68
|
+
return data[key], []
|
69
|
+
|
70
|
+
except json.JSONDecodeError:
|
71
|
+
text_response = resp.text
|
72
|
+
m3u8_match = re.search(r'"file":"([^"]*\.m3u8[^"]*)"', text_response)
|
73
|
+
if m3u8_match:
|
74
|
+
decoded_url = m3u8_match.group(1)
|
75
|
+
print(f"Debug: Regex extracted m3u8: {decoded_url}")
|
76
|
+
return decoded_url, []
|
77
|
+
|
78
|
+
print(f"Debug: Failed to decode, using original URL")
|
79
|
+
return url, []
|
80
|
+
|
81
|
+
except Exception as e:
|
82
|
+
print(f"Debug: Error decoding URL: {e}")
|
83
|
+
return url, []
|
53
84
|
|
54
|
-
|
55
|
-
|
85
|
+
def search_content(query: str):
|
86
|
+
"""Search for content on flixhq.to"""
|
87
|
+
try:
|
88
|
+
search_params = query.replace(" ", "-")
|
89
|
+
response = client.get(f"{FLIXHQ_SEARCH_URL}/{search_params}")
|
90
|
+
response.raise_for_status()
|
91
|
+
|
92
|
+
soup = BeautifulSoup(response.text, 'html.parser')
|
93
|
+
items = soup.find_all('div', class_='flw-item')
|
94
|
+
|
95
|
+
if not items:
|
96
|
+
print("No results found")
|
97
|
+
return None
|
98
|
+
|
99
|
+
results = []
|
100
|
+
urls = []
|
101
|
+
|
102
|
+
for i, item in enumerate(items[:10]):
|
103
|
+
poster_link = item.find('div', class_='film-poster')
|
104
|
+
detail_section = item.find('div', class_='film-detail')
|
105
|
+
|
106
|
+
if poster_link and detail_section:
|
107
|
+
link_elem = poster_link.find('a')
|
108
|
+
title_elem = detail_section.find('h2', class_='film-name')
|
109
|
+
|
110
|
+
if link_elem and title_elem:
|
111
|
+
href = link_elem.get('href', '')
|
112
|
+
title_link = title_elem.find('a')
|
113
|
+
title = title_link.get('title', 'Unknown Title') if title_link else 'Unknown Title'
|
114
|
+
|
115
|
+
info_elem = detail_section.find('div', class_='fd-infor')
|
116
|
+
year = ""
|
117
|
+
content_type = ""
|
118
|
+
|
119
|
+
if info_elem:
|
120
|
+
spans = info_elem.find_all('span')
|
121
|
+
if spans:
|
122
|
+
year = spans[0].text.strip() if spans else ""
|
123
|
+
if len(spans) > 1:
|
124
|
+
content_type = spans[1].text.strip()
|
125
|
+
|
126
|
+
display_title = f"{i+1}. {title}"
|
127
|
+
if year:
|
128
|
+
display_title += f" ({year})"
|
129
|
+
if content_type:
|
130
|
+
display_title += f" [{content_type}]"
|
131
|
+
|
132
|
+
results.append(display_title)
|
133
|
+
urls.append(urljoin(FLIXHQ_BASE_URL, href))
|
134
|
+
|
135
|
+
if not results:
|
136
|
+
print("No valid results found")
|
137
|
+
return None
|
138
|
+
|
139
|
+
selected = fzf_prompt(results)
|
140
|
+
if not selected:
|
141
|
+
return None
|
142
|
+
|
143
|
+
selected_index = int(selected[0]) - 1
|
144
|
+
return urls[selected_index]
|
145
|
+
|
146
|
+
except Exception as e:
|
147
|
+
print(f"Search failed: {e}")
|
148
|
+
return None
|
56
149
|
|
57
|
-
|
150
|
+
def get_tv_seasons(media_id: str):
|
151
|
+
"""Get TV show seasons using lobster's approach"""
|
152
|
+
try:
|
153
|
+
seasons_url = f"{FLIXHQ_AJAX_URL}/v2/tv/seasons/{media_id}"
|
154
|
+
|
155
|
+
response = client.get(seasons_url)
|
156
|
+
print(f"Debug: Seasons URL: {seasons_url}")
|
157
|
+
print(f"Debug: Seasons response status: {response.status_code}")
|
158
|
+
|
159
|
+
if response.status_code == 200:
|
160
|
+
# Parse like lobster: extract season title and ID from href
|
161
|
+
season_pattern = re.compile(r'href="[^"]*-(\d+)"[^>]*>([^<]*)</a>')
|
162
|
+
matches = season_pattern.findall(response.text)
|
163
|
+
|
164
|
+
seasons = []
|
165
|
+
for season_id, season_title in matches:
|
166
|
+
seasons.append({
|
167
|
+
'id': season_id,
|
168
|
+
'title': season_title.strip()
|
169
|
+
})
|
170
|
+
print(f"Debug: Found season: {season_title.strip()} (ID: {season_id})")
|
171
|
+
|
172
|
+
return seasons
|
173
|
+
|
174
|
+
return []
|
175
|
+
|
176
|
+
except Exception as e:
|
177
|
+
print(f"Failed to get TV seasons: {e}")
|
178
|
+
return []
|
58
179
|
|
59
|
-
|
180
|
+
def get_season_episodes(season_id: str):
|
181
|
+
"""Get episodes for a season using lobster's approach"""
|
182
|
+
try:
|
183
|
+
episodes_url = f"{FLIXHQ_AJAX_URL}/v2/season/episodes/{season_id}"
|
184
|
+
|
185
|
+
response = client.get(episodes_url)
|
186
|
+
print(f"Debug: Episodes URL: {episodes_url}")
|
187
|
+
print(f"Debug: Episodes response status: {response.status_code}")
|
188
|
+
|
189
|
+
if response.status_code == 200:
|
190
|
+
# Parse like lobster: look for data-id and title in nav-item elements
|
191
|
+
# First, split by class="nav-item" like lobster does
|
192
|
+
content = response.text.replace('\n', '').replace('class="nav-item"', '\nclass="nav-item"')
|
193
|
+
|
194
|
+
episode_pattern = re.compile(r'data-id="(\d+)"[^>]*title="([^"]*)"')
|
195
|
+
matches = episode_pattern.findall(content)
|
196
|
+
|
197
|
+
episodes = []
|
198
|
+
for data_id, episode_title in matches:
|
199
|
+
episodes.append({
|
200
|
+
'data_id': data_id,
|
201
|
+
'title': episode_title.strip()
|
202
|
+
})
|
203
|
+
print(f"Debug: Found episode: {episode_title.strip()} (data-id: {data_id})")
|
204
|
+
|
205
|
+
return episodes
|
206
|
+
|
207
|
+
return []
|
208
|
+
|
209
|
+
except Exception as e:
|
210
|
+
print(f"Failed to get season episodes: {e}")
|
211
|
+
return []
|
60
212
|
|
61
|
-
|
213
|
+
def get_episode_servers(data_id: str, preferred_provider: str = "Vidcloud"):
|
214
|
+
"""Get episode servers using lobster's approach"""
|
215
|
+
try:
|
216
|
+
servers_url = f"{FLIXHQ_AJAX_URL}/v2/episode/servers/{data_id}"
|
217
|
+
|
218
|
+
response = client.get(servers_url)
|
219
|
+
print(f"Debug: Servers URL: {servers_url}")
|
220
|
+
print(f"Debug: Servers response status: {response.status_code}")
|
221
|
+
|
222
|
+
if response.status_code == 200:
|
223
|
+
# Parse like lobster: look for data-id and title in nav-item elements
|
224
|
+
content = response.text.replace('\n', '').replace('class="nav-item"', '\nclass="nav-item"')
|
225
|
+
|
226
|
+
server_pattern = re.compile(r'data-id="(\d+)"[^>]*title="([^"]*)"')
|
227
|
+
matches = server_pattern.findall(content)
|
228
|
+
|
229
|
+
servers = []
|
230
|
+
for server_id, server_name in matches:
|
231
|
+
servers.append({
|
232
|
+
'id': server_id,
|
233
|
+
'name': server_name.strip()
|
234
|
+
})
|
235
|
+
print(f"Debug: Found server: {server_name.strip()} (ID: {server_id})")
|
236
|
+
|
237
|
+
# Find preferred provider like lobster does
|
238
|
+
for server in servers:
|
239
|
+
if preferred_provider.lower() in server['name'].lower():
|
240
|
+
print(f"Debug: Selected {preferred_provider} server: {server['id']}")
|
241
|
+
return server['id']
|
242
|
+
|
243
|
+
# Fallback to first server
|
244
|
+
if servers:
|
245
|
+
print(f"Debug: Using fallback server: {servers[0]['id']}")
|
246
|
+
return servers[0]['id']
|
247
|
+
|
248
|
+
return None
|
249
|
+
|
250
|
+
except Exception as e:
|
251
|
+
print(f"Failed to get episode servers: {e}")
|
252
|
+
return None
|
62
253
|
|
63
|
-
|
64
|
-
|
254
|
+
def get_embed_link(episode_id: str):
|
255
|
+
"""Get embed link from episode sources endpoint"""
|
256
|
+
try:
|
257
|
+
sources_url = f"{FLIXHQ_AJAX_URL}/episode/sources/{episode_id}"
|
258
|
+
|
259
|
+
response = client.get(sources_url)
|
260
|
+
print(f"Debug: Sources URL: {sources_url}")
|
261
|
+
print(f"Debug: Sources response status: {response.status_code}")
|
262
|
+
|
263
|
+
if response.status_code == 200:
|
264
|
+
# Extract like lobster: look for "link" in JSON response
|
265
|
+
link_match = re.search(r'"link":"([^"]*)"', response.text)
|
266
|
+
if link_match:
|
267
|
+
embed_link = link_match.group(1)
|
268
|
+
print(f"Debug: Found embed link: {embed_link}")
|
269
|
+
return embed_link
|
270
|
+
|
271
|
+
return None
|
272
|
+
|
273
|
+
except Exception as e:
|
274
|
+
print(f"Failed to get embed link: {e}")
|
275
|
+
return None
|
65
276
|
|
66
277
|
def movie():
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
)
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
else:
|
115
|
-
movie.selected = media[0]
|
278
|
+
"""Handle movie streaming"""
|
279
|
+
global selected_media, selected_subtitles
|
280
|
+
|
281
|
+
# Extract media ID from URL
|
282
|
+
media_id_match = re.search(r'/movie/[^/]*-(\d+)', get_id.selected_url)
|
283
|
+
if not media_id_match:
|
284
|
+
raise RuntimeError("Could not extract media ID from URL")
|
285
|
+
|
286
|
+
media_id = media_id_match.group(1)
|
287
|
+
print(f"Debug: Movie media ID: {media_id}")
|
288
|
+
|
289
|
+
# For movies, use the movie/episodes endpoint like lobster
|
290
|
+
try:
|
291
|
+
movie_episodes_url = f"{FLIXHQ_AJAX_URL}/movie/episodes/{media_id}"
|
292
|
+
response = client.get(movie_episodes_url)
|
293
|
+
|
294
|
+
if response.status_code == 200:
|
295
|
+
# Extract like lobster: find href with provider name
|
296
|
+
content = response.text.replace('\n', '').replace('class="nav-item"', '\nclass="nav-item"')
|
297
|
+
|
298
|
+
# Look for Vidcloud provider first
|
299
|
+
provider_pattern = re.compile(r'href="([^"]*)"[^>]*title="Vidcloud"')
|
300
|
+
match = provider_pattern.search(content)
|
301
|
+
|
302
|
+
if match:
|
303
|
+
movie_page_url = FLIXHQ_BASE_URL + match.group(1)
|
304
|
+
# Extract episode ID like lobster: -(\d+).(\d+)$ -> take the second number
|
305
|
+
episode_match = re.search(r'-(\d+)\.(\d+)$', movie_page_url)
|
306
|
+
if episode_match:
|
307
|
+
episode_id = episode_match.group(2)
|
308
|
+
print(f"Debug: Movie episode ID: {episode_id}")
|
309
|
+
|
310
|
+
# Get embed link
|
311
|
+
embed_link = get_embed_link(episode_id)
|
312
|
+
if embed_link:
|
313
|
+
selected_media = {
|
314
|
+
'file': embed_link,
|
315
|
+
'label': 'Movie Stream',
|
316
|
+
'type': 'embed'
|
317
|
+
}
|
318
|
+
selected_subtitles = []
|
319
|
+
return
|
320
|
+
|
321
|
+
except Exception as e:
|
322
|
+
print(f"Movie processing failed: {e}")
|
323
|
+
|
324
|
+
raise RuntimeError("Could not get movie stream")
|
116
325
|
|
117
326
|
def series():
|
327
|
+
"""Handle series streaming using lobster's exact approach"""
|
328
|
+
global selected_media, selected_subtitles
|
329
|
+
|
118
330
|
season = input("Enter season: ")
|
119
331
|
episode = input("Enter episode: ")
|
120
332
|
|
333
|
+
try:
|
334
|
+
season_num = int(season)
|
335
|
+
episode_num = int(episode)
|
336
|
+
except ValueError:
|
337
|
+
print("Invalid season or episode number")
|
338
|
+
raise RuntimeError("Invalid season or episode number")
|
339
|
+
|
340
|
+
# Extract media ID from URL
|
341
|
+
media_id_match = re.search(r'/tv/[^/]*-(\d+)', get_id.selected_url)
|
342
|
+
if not media_id_match:
|
343
|
+
raise RuntimeError("Could not extract media ID from URL")
|
121
344
|
|
122
|
-
|
123
|
-
|
124
|
-
GDRIVE_PLAYER_S_ENDPOINT,
|
125
|
-
params={
|
126
|
-
"imdb": get_id.imdb_ids[get_id.c-1],
|
127
|
-
"season": season,
|
128
|
-
"episode": episode,
|
129
|
-
},
|
130
|
-
).text
|
131
|
-
).group(1)
|
132
|
-
|
133
|
-
content = json.loads(
|
134
|
-
aes_decrypt(
|
135
|
-
json.loads(
|
136
|
-
httpx.get(
|
137
|
-
ENCRYPT_AJAX_ENDPOINT,
|
138
|
-
params={"id": aes_encrypt(content_id, key=SECRET, iv=IV).decode()},
|
139
|
-
headers={"x-requested-with": "XMLHttpRequest"},
|
140
|
-
).text
|
141
|
-
)["data"],
|
142
|
-
key=SECRET,
|
143
|
-
iv=IV,
|
144
|
-
)
|
145
|
-
)
|
146
|
-
|
147
|
-
|
148
|
-
series.subtitles = (_.get("file") for _ in content.get("track", {}).get("tracks", []))
|
149
|
-
|
150
|
-
media = (content.get("source", []) or []) + (content.get("source_bk", []) or [])
|
151
|
-
|
152
|
-
if not media:
|
153
|
-
raise RuntimeError("Could not find any media for playback.")
|
154
|
-
|
155
|
-
if len(media) > 2:
|
156
|
-
for content_index, source in enumerate(media):
|
157
|
-
if(content_index+1 != len(media)):
|
158
|
-
print(f" > {content_index+1} / {source['label']} / {source['type']}")
|
159
|
-
try:
|
160
|
-
while not (
|
161
|
-
(user_selection := input("Take it or leave it, index: ")).isdigit()
|
162
|
-
and (parsed_us := int(user_selection)-1) in range(content_index)
|
163
|
-
):
|
164
|
-
print("Nice joke. Now you have to TRY AGAIN!!!")
|
165
|
-
series.selected = media[parsed_us]
|
166
|
-
except KeyboardInterrupt:
|
167
|
-
exit(0)
|
168
|
-
else:
|
169
|
-
series.selected = media[0]
|
170
|
-
|
171
|
-
|
172
|
-
def get_id(query: str):
|
173
|
-
query = query.replace(" ","_")
|
345
|
+
media_id = media_id_match.group(1)
|
346
|
+
print(f"Debug: TV media ID: {media_id}")
|
174
347
|
|
175
|
-
|
348
|
+
# Step 1: Get seasons
|
349
|
+
seasons = get_tv_seasons(media_id)
|
350
|
+
if not seasons:
|
351
|
+
raise RuntimeError("Could not get seasons")
|
176
352
|
|
177
|
-
|
353
|
+
# Step 2: Find the target season (try exact match first, then positional)
|
354
|
+
target_season_id = None
|
355
|
+
for season_data in seasons:
|
356
|
+
season_title = season_data['title'].lower()
|
357
|
+
if f"season {season_num}" in season_title or f"s{season_num}" in season_title:
|
358
|
+
target_season_id = season_data['id']
|
359
|
+
break
|
178
360
|
|
179
|
-
|
180
|
-
|
361
|
+
# Fallback: assume seasons are in order
|
362
|
+
if not target_season_id and season_num <= len(seasons):
|
363
|
+
target_season_id = seasons[season_num - 1]['id']
|
181
364
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
365
|
+
if not target_season_id:
|
366
|
+
raise RuntimeError(f"Could not find season {season_num}")
|
367
|
+
|
368
|
+
print(f"Debug: Target season ID: {target_season_id}")
|
369
|
+
|
370
|
+
# Step 3: Get episodes for this season
|
371
|
+
episodes = get_season_episodes(target_season_id)
|
372
|
+
if not episodes:
|
373
|
+
raise RuntimeError(f"Could not get episodes for season {season_num}")
|
374
|
+
|
375
|
+
# Step 4: Find the target episode (assume episodes are in order)
|
376
|
+
if episode_num > len(episodes):
|
377
|
+
raise RuntimeError(f"Episode {episode_num} not found (only {len(episodes)} episodes available)")
|
378
|
+
|
379
|
+
target_episode = episodes[episode_num - 1] # Episodes are 1-indexed
|
380
|
+
print(f"Debug: Target episode: {target_episode['title']} (data-id: {target_episode['data_id']})")
|
381
|
+
|
382
|
+
# Step 5: Get episode servers and select Vidcloud
|
383
|
+
episode_id = get_episode_servers(target_episode['data_id'], "Vidcloud")
|
384
|
+
if not episode_id:
|
385
|
+
raise RuntimeError("Could not get episode server ID")
|
386
|
+
|
387
|
+
# Step 6: Get embed link
|
388
|
+
embed_link = get_embed_link(episode_id)
|
389
|
+
if not embed_link:
|
390
|
+
raise RuntimeError("Could not get embed link")
|
391
|
+
|
392
|
+
selected_media = {
|
393
|
+
'file': embed_link,
|
394
|
+
'label': f'S{season_num}E{episode_num} Stream',
|
395
|
+
'type': 'embed'
|
396
|
+
}
|
397
|
+
selected_subtitles = []
|
195
398
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
399
|
+
def get_id(query: str):
|
400
|
+
"""Search and select content"""
|
401
|
+
selected_url = search_content(query)
|
402
|
+
if not selected_url:
|
403
|
+
print("No content selected")
|
200
404
|
exit(0)
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
405
|
+
|
406
|
+
get_id.selected_url = selected_url
|
407
|
+
|
408
|
+
# Determine content type from URL
|
409
|
+
if '/movie/' in selected_url:
|
410
|
+
get_id.content_type = 'movie'
|
411
|
+
elif '/tv/' in selected_url:
|
412
|
+
get_id.content_type = 'series'
|
413
|
+
else:
|
414
|
+
get_id.content_type = 'unknown'
|
415
|
+
|
416
|
+
return selected_url
|
205
417
|
|
206
418
|
def poison():
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
419
|
+
"""Choose content type"""
|
420
|
+
if hasattr(get_id, 'content_type') and get_id.content_type in ['movie', 'series']:
|
421
|
+
if get_id.content_type == 'movie':
|
422
|
+
movie()
|
423
|
+
elif get_id.content_type == 'series':
|
424
|
+
series()
|
425
|
+
else:
|
426
|
+
ch = fzf_prompt(["movie", "series"])
|
427
|
+
if ch == "movie":
|
428
|
+
movie()
|
429
|
+
elif ch == "series":
|
430
|
+
series()
|
431
|
+
else:
|
432
|
+
exit(0)
|
216
433
|
else:
|
217
|
-
|
218
|
-
|
219
|
-
|
434
|
+
ch = fzf_prompt(["movie", "series"])
|
435
|
+
if ch == "movie":
|
436
|
+
movie()
|
437
|
+
elif ch == "series":
|
438
|
+
series()
|
439
|
+
else:
|
440
|
+
exit(0)
|
220
441
|
|
221
442
|
def determine_path() -> str:
|
222
|
-
|
223
443
|
plt = platform.system()
|
224
|
-
|
225
444
|
if plt == "Windows":
|
226
445
|
return f"C://Users//{os.getenv('username')}//Downloads"
|
227
|
-
|
228
|
-
elif (plt == "Linux"):
|
446
|
+
elif plt == "Linux":
|
229
447
|
return f"/home/{os.getlogin()}/Downloads"
|
230
|
-
|
231
|
-
elif (plt == "Darwin"):
|
448
|
+
elif plt == "Darwin":
|
232
449
|
return f"/Users/{os.getlogin()}/Downloads"
|
233
|
-
|
234
450
|
else:
|
235
451
|
print("[!] Make an issue for your OS.")
|
236
452
|
exit(0)
|
237
453
|
|
238
454
|
def dlData(path: str = determine_path()):
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
download(path, query, selected['file'], DEFAULT_MEDIA_REFERER)
|
455
|
+
global selected_media
|
456
|
+
if selected_media:
|
457
|
+
decoded_url, subs = decode_url(selected_media['file'])
|
458
|
+
download(path, query, decoded_url, FLIXHQ_BASE_URL)
|
459
|
+
else:
|
460
|
+
print("No media selected for download")
|
246
461
|
|
247
462
|
def provideData():
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
play(selected['file'], query, DEFAULT_MEDIA_REFERER, subtitles)
|
255
|
-
|
463
|
+
global selected_media, selected_subtitles
|
464
|
+
if selected_media:
|
465
|
+
decoded_url, subs = decode_url(selected_media['file'])
|
466
|
+
play(decoded_url, query, FLIXHQ_BASE_URL, subs)
|
467
|
+
else:
|
468
|
+
print("No media selected for playback")
|
256
469
|
|
257
470
|
def init():
|
258
471
|
ch = fzf_prompt(["play", "download", "exit"])
|
259
|
-
|
260
472
|
if ch == "play":
|
261
473
|
provideData()
|
262
474
|
elif ch == "download":
|
@@ -264,7 +476,15 @@ def init():
|
|
264
476
|
else:
|
265
477
|
exit(0)
|
266
478
|
|
479
|
+
if len(sys.argv) == 1:
|
480
|
+
query = input("Search: ")
|
481
|
+
if query == "":
|
482
|
+
print("ValueError: no query parameter provided")
|
483
|
+
exit(0)
|
484
|
+
else:
|
485
|
+
query = " ".join(sys.argv[1:])
|
267
486
|
|
268
|
-
|
269
|
-
|
487
|
+
get_id(query)
|
488
|
+
poison()
|
270
489
|
init()
|
490
|
+
|
flix_cli/core/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__core__ = "1.6.
|
1
|
+
__core__ = "1.6.4"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import platform
|
1
|
+
import platform as plt
|
2
2
|
import subprocess
|
3
3
|
|
4
4
|
MPV_EXECUTABLE = "mpv"
|
@@ -7,7 +7,7 @@ IINA_EXECUTABLE = "iina"
|
|
7
7
|
|
8
8
|
def play(file, name, referer, subtitles):
|
9
9
|
try:
|
10
|
-
if(
|
10
|
+
if(plt.system() == 'Linux' or plt.system() == 'Windows'):
|
11
11
|
args = [
|
12
12
|
MPV_EXECUTABLE,
|
13
13
|
file,
|
@@ -20,7 +20,7 @@ def play(file, name, referer, subtitles):
|
|
20
20
|
|
21
21
|
mpv_process.wait()
|
22
22
|
|
23
|
-
elif(
|
23
|
+
elif(plt.system() == 'Darwin'):
|
24
24
|
args = [
|
25
25
|
IINA_EXECUTABLE,
|
26
26
|
"--no-stdin",
|
@@ -0,0 +1,22 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: flix-cli
|
3
|
+
Version: 1.6.4
|
4
|
+
Summary: A high efficient, powerful and fast movie scraper.
|
5
|
+
License: GPLv3
|
6
|
+
Author: DemonKingSwarn
|
7
|
+
Author-email: rockingswarn@gmail.com
|
8
|
+
Requires-Python: >=3.10,<4.0
|
9
|
+
Classifier: License :: Other/Proprietary License
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
15
|
+
Requires-Dist: beautifulsoup4 (==4.10.0)
|
16
|
+
Requires-Dist: colorama (==0.4.5)
|
17
|
+
Requires-Dist: httpx (==0.28.1)
|
18
|
+
Requires-Dist: krfzf-py (==0.0.4)
|
19
|
+
Requires-Dist: pycryptodomex (==3.14.1)
|
20
|
+
Description-Content-Type: text/plain
|
21
|
+
|
22
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
flix_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
flix_cli/__main__.py,sha256=_KHjy8tMz3gOf6eDvbRj0LuX98gwO4iJfKYxa8Kbejk,114
|
3
|
+
flix_cli/core/__flix_cli__.py,sha256=f9wEHlNuWrMHMOR6Vz-TQjzBv8RlWoE7zxa2TEYqiDs,17264
|
4
|
+
flix_cli/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
flix_cli/core/__version__.py,sha256=JVpd7ktPBtiYU8h7QWDa1Xtss5Fp3XenGq9g5Dh1ubk,19
|
6
|
+
flix_cli/core/utils/__downloader__.py,sha256=o1dZ49ambnTslCdX8jYGYnijo0WAZxegqBfOHygQQYU,341
|
7
|
+
flix_cli/core/utils/__player__.py,sha256=RsPO9pg1nPrNLlp8FCwKIV1d33lnZ_lL2LFYo5imsgg,1159
|
8
|
+
flix_cli-1.6.4.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
9
|
+
flix_cli-1.6.4.dist-info/METADATA,sha256=mH08dbAbJ7MWwD36RD039RwW_QdJNueKpwAYASSj8Lk,737
|
10
|
+
flix_cli-1.6.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
11
|
+
flix_cli-1.6.4.dist-info/entry_points.txt,sha256=q0iV-R8zPxC5MNHTtsPWas4fM_2qRoR0s30-N4Sf_-E,58
|
12
|
+
flix_cli-1.6.4.dist-info/RECORD,,
|
@@ -1,143 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: flix-cli
|
3
|
-
Version: 1.6.1
|
4
|
-
Summary: A high efficient, powerful and fast movie scraper.
|
5
|
-
License: GPLv3
|
6
|
-
Author: DemonKingSwarn
|
7
|
-
Author-email: rockingswarn@gmail.com
|
8
|
-
Requires-Python: >=3.10,<4.0
|
9
|
-
Classifier: License :: Other/Proprietary License
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
12
|
-
Classifier: Programming Language :: Python :: 3.11
|
13
|
-
Requires-Dist: beautifulsoup4 (==4.10.0)
|
14
|
-
Requires-Dist: colorama (==0.4.5)
|
15
|
-
Requires-Dist: httpx (==0.23.0)
|
16
|
-
Requires-Dist: krfzf-py (>=0.0.4,<0.0.5)
|
17
|
-
Requires-Dist: pycryptodomex (==3.14.1)
|
18
|
-
Description-Content-Type: text/markdown
|
19
|
-
|
20
|
-
<p align="center">
|
21
|
-
<br>
|
22
|
-
<a href="https://github.com/demonkingswarn/flix-cli"><img src="https://img.shields.io/github/stars/demonkingswarn/flix-cli?color=orange&logo=github&style=flat-square"></a>
|
23
|
-
<a href="http://makeapullrequest.com"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg"></a>
|
24
|
-
<img src="https://img.shields.io/badge/os-linux-brightgreen">
|
25
|
-
<img src="https://img.shields.io/badge/os-mac-brightgreen">
|
26
|
-
<img src="https://img.shields.io/badge/os-windows-brightgreen">
|
27
|
-
<img src="https://img.shields.io/badge/os-android-brightgreen">
|
28
|
-
<br>
|
29
|
-
</p>
|
30
|
-
<h1 align="center">
|
31
|
-
FLIX-CLI
|
32
|
-
</h1>
|
33
|
-
<br>
|
34
|
-
<h3 align="center">
|
35
|
-
A high efficient, powerful and fast movie scraper.
|
36
|
-
</h3>
|
37
|
-
<br>
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
https://user-images.githubusercontent.com/69480361/193068496-ec0e418d-375b-4a14-be75-e3ac10d853b4.mp4
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
<hr>
|
46
|
-
|
47
|
-
# Overview
|
48
|
-
|
49
|
-
- [Installation](#installation)
|
50
|
-
1. [PIP Installation](#1-pip-installs-packages-aka-pip-installation)
|
51
|
-
2. [Source Code Download](#2-source-code-download)
|
52
|
-
3. [Android Installation](#3-android-installation)
|
53
|
-
- [Dependencies](#dependencies)
|
54
|
-
- [Usage](#usage)
|
55
|
-
- [Support](#support)
|
56
|
-
- [Provider](#provider)
|
57
|
-
- [Project Disclaimer](#project-disclaimer)
|
58
|
-
- [Honourable Mentions](#honourable-mentions)
|
59
|
-
|
60
|
-
# Installation
|
61
|
-
<i>for dependencies <a href="https://github.com/DemonKingSwarn/flix-cli#dependencies">(see below)</a>.</i>
|
62
|
-
|
63
|
-
This project can be installed on to your device via different mechanisms, these mechanisms are listed below in the order of ease.
|
64
|
-
|
65
|
-
## 1. PIP Installs Packages aka PIP Installation
|
66
|
-
```sh
|
67
|
-
pip install flix-cli
|
68
|
-
```
|
69
|
-
|
70
|
-
## 2. Source Code Download
|
71
|
-
``` sh
|
72
|
-
git clone https://github.com/demonkingswarn/flix-cli
|
73
|
-
```
|
74
|
-
|
75
|
-
Given that you have `git` installed, you can clone the repository from GitHub. If you do not have or want to deal with installation of `git`, you can simply download the repository using <a href="https://github.com/demonkingswarn/flix-cli/archive/refs/heads/master.zip">this link</a>.
|
76
|
-
|
77
|
-
After the repository is downloaded and placed in an appropriate directory, you can, either use [`runner.py`](https://github.com/demonkingswarn/flix-cli/blob/master/runner.py) to use the project without installation or use [`setup.py`](https://github.com/demonkingswarn/flix-cli/blob/master/setup.py) to proceed with the installation.
|
78
|
-
|
79
|
-
The former can be done via:
|
80
|
-
```sh
|
81
|
-
python runner.py
|
82
|
-
```
|
83
|
-
The later can be done via:
|
84
|
-
```sh
|
85
|
-
pip install .
|
86
|
-
```
|
87
|
-
Both commands are to be executed from the directory where the repository is located.
|
88
|
-
|
89
|
-
<b>Additional information</b>: You <b>must</b> have Python installed <b>and</b> in PATH to use this project properly. Your Python executable may be `py` or `python` or `python3`. <b>Only Python 3.6 and higher versions are supported by the project.</b>
|
90
|
-
|
91
|
-
## 3. Android Installation
|
92
|
-
Install termux <a href="https://termux.com">(Guide)</a>
|
93
|
-
```sh
|
94
|
-
pkg up -y
|
95
|
-
pip install flix-cli
|
96
|
-
echo "#\!/data/data/com.termux/files/usr/bin/sh" > $PREFIX/bin/mpv
|
97
|
-
echo 'am start --user 0 -a android.intent.action.VIEW -d "$1" -n is.xyz.mpv/.MPVActivity &' >> $PREFIX/bin/mpv
|
98
|
-
chmod +x $PREFIX/bin/mpv
|
99
|
-
```
|
100
|
-
|
101
|
-
For it to be able to stream you need to add referrer in mpv by opening mpv <a href="https://play.google.com/store/apps/details?id=is.xyz.mpv">(playstore version)</a>, going into Settings -> Android -> Edit mpv.conf and adding
|
102
|
-
```sh
|
103
|
-
referrer="https://membed1.com"
|
104
|
-
```
|
105
|
-
|
106
|
-
# Dependencies
|
107
|
-
- [`mpv`](https://mpv.io) - Video Player
|
108
|
-
- [`iina`](https://iina.io) - Alternate video player for MacOS
|
109
|
-
- [`ffmpeg`](https://github.com/FFmpeg/FFmpeg) - Download manager
|
110
|
-
- [`fzf`](https://github.com/junegunn/fzf) - for selection menu
|
111
|
-
|
112
|
-
# Usage
|
113
|
-
|
114
|
-
```sh
|
115
|
-
Usage: flix-cli [ARGS]...
|
116
|
-
|
117
|
-
Options:
|
118
|
-
download Download your favourite movie by query.
|
119
|
-
play Stream your favourite movie by query.
|
120
|
-
```
|
121
|
-
|
122
|
-
# Support
|
123
|
-
You can contact the developer directly via this <a href="mailto:swarn@demonkingswarn.ml">email</a>. However, the most recommended way is to head to the discord server.
|
124
|
-
|
125
|
-
<a href="https://discord.gg/JF85vTkDyC"><img src="https://invidget.switchblade.xyz/JF85vTkDyC"></a>
|
126
|
-
|
127
|
-
If you run into issues or want to request a new feature, you are encouraged to make a GitHub issue, won't bite you, trust me.
|
128
|
-
|
129
|
-
# Provider
|
130
|
-
| Website | Available Qualities | Status / Elapsed Time | Content Extension |
|
131
|
-
| :------------------------------------------: | :-----------------: | :----: | :-----------------: |
|
132
|
-
| [gdriveplayer](https://database.gdriveplayer.us/player.php) | 720p, 1080p | <img height="25" src="https://github.com/DemonKingSwarn/flix-status/raw/master/images/gdriveplayer.jpg"> | MP4 |
|
133
|
-
|
134
|
-
# Project Disclaimer
|
135
|
-
The disclaimer of the project can be found <a href="https://github.com/demonkingswarn/flix-cli/blob/master/disclaimer.org">here</a>.
|
136
|
-
|
137
|
-
# Honourable Mentions
|
138
|
-
|
139
|
-
- [`animdl`](https://github.com/justfoolingaround/animdl): Ridiculously efficient, fast and light-weight (supports most sources: animixplay, 9anime...) (Python)
|
140
|
-
- [`ani-cli`](https://github.com/pystardust/ani-cli): A cli tool to browse and play anime. (Shell)
|
141
|
-
- [`mov-cli`](https://github.com/mov-cli/mov-cli): [WIP] watch movies and webseries from the cli. (Python/Shell)
|
142
|
-
- [`kami`](https://github.com/mrfluffy-dev/kami): Read light novels and watch anime in your terminal. (Rust)
|
143
|
-
|
flix_cli-1.6.1.dist-info/RECORD
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
flix_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
flix_cli/__main__.py,sha256=_KHjy8tMz3gOf6eDvbRj0LuX98gwO4iJfKYxa8Kbejk,114
|
3
|
-
flix_cli/core/__flix_cli__.py,sha256=KQklDizOd-uV6AQDXBdP1yxa7PlaP0qY036MTgMIURA,7164
|
4
|
-
flix_cli/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
flix_cli/core/__version__.py,sha256=v3i_haTnxqJrywmuh1_ptIjwcqYeAwdJL5QtEJwq5HE,19
|
6
|
-
flix_cli/core/utils/__downloader__.py,sha256=o1dZ49ambnTslCdX8jYGYnijo0WAZxegqBfOHygQQYU,341
|
7
|
-
flix_cli/core/utils/__player__.py,sha256=EbMwuYZ7lQ-UzpXo-6OkhhqOajvIdt6m-BANjNFVQuk,1167
|
8
|
-
flix_cli-1.6.1.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
|
9
|
-
flix_cli-1.6.1.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
10
|
-
flix_cli-1.6.1.dist-info/METADATA,sha256=4NIfKpry_6kog6vlny6108H_SJouK5TyAchRkySzF7I,5869
|
11
|
-
flix_cli-1.6.1.dist-info/entry_points.txt,sha256=q0iV-R8zPxC5MNHTtsPWas4fM_2qRoR0s30-N4Sf_-E,58
|
12
|
-
flix_cli-1.6.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|