@officebeats/matrix-iptv-cli 4.0.18 → 4.0.20

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.
package/old_player.rs DELETED
@@ -1,404 +0,0 @@
1
- use std::path::PathBuf;
2
- use std::sync::Arc;
3
- use std::sync::Mutex;
4
- use crate::config::PlayerEngine;
5
-
6
- #[cfg(not(target_arch = "wasm32"))]
7
- use std::process::{Child, Command, Stdio};
8
-
9
- #[cfg(not(target_arch = "wasm32"))]
10
-
11
-
12
- #[cfg(not(target_arch = "wasm32"))]
13
-
14
-
15
- #[cfg(target_arch = "wasm32")]
16
- use web_sys::window;
17
-
18
- #[derive(Clone)]
19
- pub struct Player {
20
- #[cfg(not(target_arch = "wasm32"))]
21
- process: Arc<Mutex<Option<Child>>>,
22
- #[cfg(not(target_arch = "wasm32"))]
23
- ipc_path: Arc<Mutex<Option<PathBuf>>>,
24
- }
25
-
26
- impl Player {
27
- pub fn new() -> Self {
28
- #[cfg(not(target_arch = "wasm32"))]
29
- {
30
- Self {
31
- process: Arc::new(Mutex::new(None)),
32
- ipc_path: Arc::new(Mutex::new(None)),
33
- }
34
- }
35
- #[cfg(target_arch = "wasm32")]
36
- {
37
- Self {}
38
- }
39
- }
40
-
41
- /// Start the selected player engine and return the IPC pipe path for monitoring
42
- #[cfg(not(target_arch = "wasm32"))]
43
- pub async fn play(&self, url: &str, engine: PlayerEngine, use_default_mpv: bool, smooth_motion: bool) -> Result<(), anyhow::Error> {
44
- // Pre-flight check: Verify stream is reachable before launching player
45
- // This detects dead redirects/DNS issues early and notifies the user
46
- self.check_stream_health(url).await?;
47
-
48
- self.stop();
49
-
50
- match engine {
51
- PlayerEngine::Mpv => self.play_mpv(url, use_default_mpv, smooth_motion),
52
- PlayerEngine::Vlc => self.play_vlc(url, smooth_motion),
53
- }
54
- }
55
-
56
- async fn check_stream_health(&self, url: &str) -> Result<(), anyhow::Error> {
57
- // Build a client that mimics the player's behavior (Chrome UA)
58
- let client = reqwest::Client::builder()
59
- .timeout(std::time::Duration::from_secs(10))
60
- .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
61
- .danger_accept_invalid_certs(true)
62
- .build()
63
- .unwrap_or_else(|_| reqwest::Client::new());
64
-
65
- // We use a stream request but abort immediately to check connectivity/headers.
66
- // HEAD often fails on some IPTV servers, so a started GET is safer logic-wise.
67
- let mut result = client.get(url).send().await;
68
-
69
- // Resilience: Fallback to DoH if DNS fails for the stream health check
70
- if let Err(ref e) = result {
71
- if crate::doh::is_dns_error(e) {
72
- #[cfg(debug_assertions)]
73
- println!("DEBUG: Health check DNS error detected for {}. Trying DoH fallback...", url);
74
-
75
- if let Some(resp) = crate::doh::try_doh_fallback(&client, url).await {
76
- result = Ok(resp);
77
- }
78
- }
79
- }
80
-
81
- match result {
82
- Ok(resp) => {
83
- if resp.status().is_success() || resp.status().is_redirection() {
84
- Ok(())
85
- } else {
86
- Err(anyhow::anyhow!("Stream returned error status: {} (Server might be offline/blocking)", resp.status()))
87
- }
88
- },
89
- Err(e) => {
90
- // Provide a user-friendly error description using shared DNS detection
91
- if crate::doh::is_dns_error(&e) {
92
- Err(anyhow::anyhow!("Stream Server Unreachable. The host likely does not exist or is blocked (DNS Error). Details: {}", e))
93
- } else if e.is_connect() || e.is_timeout() {
94
- Err(anyhow::anyhow!("Stream Connection Failed. Server may be slow or offline. Details: {}", e))
95
- } else {
96
- Err(anyhow::anyhow!("Stream Check Failed: {}", e))
97
- }
98
- }
99
- }
100
- }
101
-
102
- #[cfg(not(target_arch = "wasm32"))]
103
- fn play_mpv(&self, url: &str, use_default_mpv: bool, smooth_motion: bool) -> Result<(), anyhow::Error> {
104
-
105
- // Find mpv executable, checking PATH and common installation locations
106
- let mpv_path = crate::setup::get_mpv_path().ok_or_else(|| {
107
- let hint = if cfg!(target_os = "macos") {
108
- "\n\nHint: On macOS with Homebrew:\n\
109
- - Apple Silicon: mpv is typically at /opt/homebrew/bin/mpv\n\
110
- - Intel Mac: mpv is typically at /usr/local/bin/mpv\n\n\
111
- To fix, add Homebrew to your PATH:\n\
112
- export PATH=\"/opt/homebrew/bin:$PATH\"\n\
113
- (Add this line to ~/.zshrc or ~/.bash_profile)"
114
- } else {
115
- ""
116
- };
117
- anyhow::anyhow!(
118
- "mpv not found. Please install mpv and ensure it's in your PATH.{}",
119
- hint
120
- )
121
- })?;
122
-
123
- // Create a unique IPC path for this session
124
- let pipe_name = if cfg!(target_os = "windows") {
125
- format!("\\\\.\\pipe\\mpv_ipc_{}", std::process::id())
126
- } else {
127
- format!("/tmp/mpv_ipc_{}", std::process::id())
128
- };
129
-
130
- let mut cmd = Command::new(&mpv_path);
131
- cmd.arg(url)
132
- .arg("--geometry=1280x720") // Start in 720p window (user preference)
133
- .arg("--force-window") // Ensure window opens even if audio-only initially
134
- .arg("--no-fs") // DISABLING FULLSCREEN - Force Windowed Mode
135
- .arg("--osc=yes"); // Enable On Screen Controller for usability
136
-
137
- // Apply smooth motion interpolation if enabled
138
- if smooth_motion {
139
- cmd.arg("--video-sync=display-resample") // Smooth motion sync (required for interpolation)
140
- .arg("--interpolation=yes") // Frame generation / motion smoothing
141
- .arg("--tscale=linear") // Soap opera effect - smooth motion blending (GPU friendly)
142
- .arg("--tscale-clamp=0.0"); // Allow full blending for maximum smoothness
143
- }
144
-
145
- // Only apply optimizations if not using default MPV settings
146
- if !use_default_mpv {
147
- cmd.arg("--cache=yes")
148
- .arg("--cache-pause=yes") // Pause when cache is starved
149
- .arg("--cache-pause-wait=5") // Wait for 5 seconds of cache before resuming (builds a buffer against live edge)
150
- .arg("--cache-pause-initial=yes") // Ensure we buffer 5 seconds initially
151
- .arg("--network-timeout=10") // Increased timeout to prevent premature EOF closures
152
- .arg("--hls-bitrate=max") // Force highest quality HLS to prevent bitrate switching loops
153
- .arg("--tls-verify=no") // Ignore certificate errors for internal/sketchy HTTPS streams
154
- .arg("--hr-seek=yes") // Precise seeking for better buffer recovery
155
- // NETWORK TURBO MODE: Aggressive Caching for Stability
156
- .arg("--demuxer-max-bytes=512MiB") // Doubled cache to 512MB
157
- .arg("--demuxer-max-back-bytes=128MiB") // Increase back buffer for seeking/rewind
158
- .arg("--demuxer-readahead-secs=60") // Buffer 1 full minute ahead (Adaptive Buffering)
159
- .arg("--stream-buffer-size=8MiB") // Low-level socket buffer
160
- .arg("--load-unsafe-playlists=yes") // Stay alive through malformed HLS fragments
161
- .arg("--framedrop=vo") // Drop frames gracefully if GPU lags
162
- .arg("--vd-lavc-fast") // Enable fast decoding optimizations
163
- .arg("--vd-lavc-skiploopfilter=all") // Major CPU saver for low-end machines
164
- .arg("--vd-lavc-threads=0") // Maximize thread usage for decoding
165
- // LOW-END FRIENDLY UPSCALING (catmull_rom: good quality, low GPU cost)
166
- .arg("--scale=catmull_rom") // Clean upscaling, ~25% faster than spline36
167
- .arg("--cscale=catmull_rom") // Matching chroma scaler
168
- .arg("--dscale=catmull_rom") // Consistent downscaling
169
- .arg("--scale-antiring=0.7") // Reduce haloing
170
- .arg("--cscale-antiring=0.7")
171
- .arg("--hwdec=auto-copy") // More compatible hardware decoding
172
- // RECONNECT TURBO: Auto-reconnect on network drops (avoiding aggressive EOF looping)
173
- .arg("--stream-lavf-o=reconnect_on_http_error=4xx,5xx,reconnect_on_network_error=1,reconnect_streamed=1,reconnect_delay_max=5,fflags=+genpts+igndts");
174
-
175
- if cfg!(target_os = "windows") {
176
- cmd.arg("--d3d11-flip=yes") // Modern Windows presentation (faster)
177
- .arg("--gpu-api=d3d11"); // Force D3D11 (faster than OpenGL on Windows)
178
- } else if cfg!(target_os = "macos") {
179
- cmd.arg("--gpu-api=opengl"); // Generally safe default for macOS mpv
180
- }
181
- }
182
-
183
- // Common settings for both modes
184
- cmd.arg("--msg-level=all=no")
185
- .arg("--term-status-msg=no")
186
- .arg("--input-terminal=no") // Ignore terminal for input
187
- .arg("--terminal=no") // Completely disable terminal interactions
188
- // USER AGENT MASQUERADE: Modern Chrome to avoid throttling
189
- .arg("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
190
- // Keep window open if playback fails to see error (optional, maybe off for prod)
191
- .arg("--keep-open=no")
192
- // Logging for troubleshooting
193
- .arg("--log-file=mpv_playback.log")
194
- // IPC for status monitoring
195
- .arg(format!("--input-ipc-server={}", pipe_name));
196
-
197
- // Disconnect from terminal input/output to prevent hotkey conflicts
198
- cmd.stdin(Stdio::null())
199
- .stdout(Stdio::null())
200
- .stderr(Stdio::null());
201
-
202
- let child = cmd.spawn();
203
-
204
- match child {
205
- Ok(child) => {
206
- {
207
- let mut guard = self.process.lock().map_err(|e| {
208
- anyhow::anyhow!("Failed to lock process mutex: {}", e)
209
- })?;
210
- *guard = Some(child);
211
- }
212
- {
213
- let mut ipc_guard = self.ipc_path.lock().map_err(|e| {
214
- anyhow::anyhow!("Failed to lock IPC path mutex: {}", e)
215
- })?;
216
- *ipc_guard = Some(PathBuf::from(&pipe_name));
217
- }
218
- Ok(())
219
- }
220
- Err(e) => {
221
- let hint = if cfg!(target_os = "macos") {
222
- format!(
223
- "\n\nSearched for mpv at: {}\n\
224
- Hint: On Apple Silicon, Homebrew installs to /opt/homebrew/bin.\n\
225
- Add this to your ~/.zshrc: export PATH=\"/opt/homebrew/bin:$PATH\"",
226
- mpv_path
227
- )
228
- } else {
229
- String::new()
230
- };
231
- Err(anyhow::anyhow!(
232
- "Failed to start mpv: {}.{}",
233
- e, hint
234
- ))
235
- }
236
- }
237
- }
238
-
239
- #[cfg(not(target_arch = "wasm32"))]
240
- fn play_vlc(&self, url: &str, smooth_motion: bool) -> Result<(), anyhow::Error> {
241
- // Find vlc executable
242
- let vlc_path = crate::setup::get_vlc_path().ok_or_else(|| {
243
- anyhow::anyhow!("VLC not found. Please install VLC.")
244
- })?;
245
-
246
- let mut cmd = Command::new(&vlc_path);
247
-
248
- // Add Referrer validation (Common anti-scraping measure)
249
- // Manual parsing to avoid adding 'url' crate dependency
250
- if let Some(scheme_end) = url.find("://") {
251
- let rest = &url[scheme_end + 3..];
252
- if let Some(path_start) = rest.find('/') {
253
- let host = &rest[..path_start];
254
- let base = format!("{}://{}/", &url[..scheme_end], host);
255
- cmd.arg(format!("--http-referrer={}", base));
256
- }
257
- }
258
-
259
- cmd.arg(url)
260
- .arg("--no-video-title-show")
261
- .arg("--http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
262
- .arg("--http-reconnect")
263
- .arg("--http-continuous")
264
- .arg("--clock-jitter=500") // Allow more jitter in stream clock
265
- .arg("--network-caching=15000") // 15 second buffer for TS streams
266
- .arg("--gnutls-verify-trust-ee=no"); // For VLC HTTPS stability
267
-
268
- // Apply smooth motion (deinterlacing) if enabled
269
- if smooth_motion {
270
- cmd.arg("--video-filter=deinterlace")
271
- .arg("--deinterlace-mode=bob");
272
- }
273
- // DISCONNECT from terminal
274
- cmd.stdin(Stdio::null())
275
- .stdout(Stdio::null())
276
- .stderr(Stdio::null());
277
-
278
- #[cfg(windows)]
279
- {
280
- use std::os::windows::process::CommandExt;
281
- const DETACHED_PROCESS: u32 = 0x00000008;
282
- cmd.creation_flags(DETACHED_PROCESS);
283
- }
284
-
285
- let child = cmd.spawn()?;
286
-
287
- {
288
- let mut guard = self.process.lock().map_err(|e| {
289
- anyhow::anyhow!("Failed to lock process mutex: {}", e)
290
- })?;
291
- *guard = Some(child);
292
- }
293
-
294
- Ok(())
295
- }
296
-
297
- /// Read the last few lines of the player logs to find errors
298
- pub fn get_last_error_from_log(&self) -> Option<String> {
299
- let logs = ["mpv_playback.log", "vlc_playback.log"];
300
-
301
- for log_file in logs {
302
- if let Ok(content) = std::fs::read_to_string(log_file) {
303
- let lines: Vec<&str> = content.lines().rev().take(15).collect();
304
- for line in lines {
305
- let lower = line.to_lowercase();
306
- if lower.contains("error") || lower.contains("failed") || lower.contains("fatal") {
307
- // Clean up common VLC/MPV prefixes for cleaner UI display
308
- let cleaned = line.split("]: ").last().unwrap_or(line);
309
- return Some(cleaned.to_string());
310
- }
311
- }
312
- }
313
- }
314
- None
315
- }
316
-
317
- /// Check if MPV is still running (process alive)
318
- #[cfg(not(target_arch = "wasm32"))]
319
- pub fn is_running(&self) -> bool {
320
- if let Ok(mut guard) = self.process.lock() {
321
- if let Some(ref mut child) = *guard {
322
- // try_wait returns Ok(Some(status)) if exited, Ok(None) if still running
323
- match child.try_wait() {
324
- Ok(Some(_)) => false, // Process has exited
325
- Ok(None) => true, // Still running
326
- Err(_) => false, // Error, assume not running
327
- }
328
- } else {
329
- false
330
- }
331
- } else {
332
- false // Mutex poisoned, assume not running
333
- }
334
- }
335
-
336
- /// Wait for MPV to actually start playing by polling process status
337
- /// Returns Ok(true) if playback confirmed, Ok(false) if process died, Err on timeout
338
- #[cfg(not(target_arch = "wasm32"))]
339
- pub async fn wait_for_playback(&self, timeout_ms: u64) -> Result<bool, anyhow::Error> {
340
- use tokio::time::{sleep, Duration, Instant};
341
-
342
- let start = Instant::now();
343
- let timeout = Duration::from_millis(timeout_ms);
344
-
345
- // Give MPV a moment to initialize
346
- sleep(Duration::from_millis(500)).await;
347
-
348
- // Poll until process is confirmed running and has had time to buffer
349
- while start.elapsed() < timeout {
350
- if !self.is_running() {
351
- // Process died, playback failed
352
- return Ok(false);
353
- }
354
-
355
- // Check if process has been alive for at least 2 seconds
356
- // This indicates MPV successfully connected and is playing
357
- if start.elapsed() > Duration::from_millis(2000) {
358
- return Ok(true);
359
- }
360
-
361
- sleep(Duration::from_millis(200)).await;
362
- }
363
-
364
- // If we reached here and process is still running, consider it a success
365
- Ok(self.is_running())
366
- }
367
-
368
- #[cfg(target_arch = "wasm32")]
369
- pub fn play(&self, url: &str, _engine: PlayerEngine, _use_default_mpv: bool, _smooth_motion: bool) -> Result<(), anyhow::Error> {
370
- self.stop();
371
- if let Some(win) = window() {
372
- let _ = win.alert_with_message(&format!("Play stream: {}", url));
373
- }
374
- Ok(())
375
- }
376
-
377
- #[cfg(not(target_arch = "wasm32"))]
378
- pub fn stop(&self) {
379
- if let Ok(mut guard) = self.process.lock() {
380
- if let Some(mut child) = guard.take() {
381
- let _ = child.kill();
382
- let _ = child.wait();
383
- }
384
- }
385
-
386
- if let Ok(mut ipc_guard) = self.ipc_path.lock() {
387
- *ipc_guard = None;
388
- }
389
- }
390
-
391
- #[cfg(target_arch = "wasm32")]
392
- pub fn stop(&self) {
393
- if let Some(_win) = window() {
394
- web_sys::console::log_1(&"Stopping stream".into());
395
- }
396
- }
397
- }
398
-
399
- impl Drop for Player {
400
- fn drop(&mut self) {
401
- #[cfg(not(target_arch = "wasm32"))]
402
- self.stop();
403
- }
404
- }
package/response.html DELETED
@@ -1,20 +0,0 @@
1
- <!doctype html>
2
- <html style="height:100%">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title></title>
7
- <script type="text/javascript">
8
- var url="https://myhomenetwork.att.com";
9
- var reason=["phishing"];
10
- </script>
11
-
12
- </head>
13
- <body style="margin: 0 auto; padding: 0 auto; width:100%; height: 100%; overflow : hidden;">
14
- <iframe src="" id="frame" seamless="seamless" style="margin: 0 auto; padding: 0 auto; border: 0; width:100%; height: 100%" > </iframe>
15
-
16
- <script type="text/javascript">
17
- document.getElementById('frame').src = url+"/networkprotected/index.html?reason="+reason[0]+"&source="+encodeURIComponent(window.location.href);
18
- </script>
19
- </body>
20
- </html>
@@ -1,50 +0,0 @@
1
- import urllib.request
2
- import json
3
- import time
4
-
5
- accounts = [
6
- {"name": "Strong 8K", "url": "http://pledge78502.cdn-akm.me:80", "user": "7c34d33c9e21", "pass": "037dacb169"},
7
- {"name": "Trex", "url": "http://line.offcial-trex.pro", "user": "3a6aae52fb", "pass": "39c165888139"},
8
- {"name": "Strong8k2-PC", "url": "http://zfruvync.duperab.xyz", "user": "PE1S9S8U", "pass": "11EZZUMW"},
9
- {"name": "Mega OTT 1", "url": "http://line.4smart.in", "user": "45Z88W6", "pass": "Z7PHTX3"}
10
- ]
11
-
12
- print("=== Matrix IPTV Performance Benchmark (Python) ===")
13
-
14
- for acc in accounts:
15
- print(f"\nProcessing: {acc['name']}")
16
-
17
- try:
18
- # 1. Categories
19
- start = time.time()
20
- cat_url = f"{acc['url']}/player_api.php?username={acc['user']}&password={acc['pass']}&action=get_live_categories"
21
- with urllib.request.urlopen(cat_url, timeout=30) as resp:
22
- cats = json.loads(resp.read().decode())
23
- duration = time.time() - start
24
- print(f" āœ… Categories: {len(cats)} items in {duration:.2f}s")
25
-
26
- # 2. Streams & MSNBC
27
- print(" šŸ” Searching for MSNBC...")
28
- start = time.time()
29
- streams_url = f"{acc['url']}/player_api.php?username={acc['user']}&password={acc['pass']}&action=get_live_streams"
30
- with urllib.request.urlopen(streams_url, timeout=120) as resp:
31
- streams = json.loads(resp.read().decode())
32
- duration = time.time() - start
33
- print(f" āœ… Streams: {len(streams)} items in {duration:.2f}s")
34
-
35
- msnbc = [s for s in streams if "MSNBC" in s.get("name", "")]
36
- if msnbc:
37
- print(f" šŸ“ Found {len(msnbc)} MSNBC streams:")
38
- for s in msnbc[:3]:
39
- stream_id = s.get("stream_id")
40
- name = s.get("name")
41
- play_url = f"{acc['url']}/live/{acc['user']}/{acc['pass']}/{stream_id}.ts"
42
- print(f" - [{stream_id}] {name}")
43
- print(f" Link: {play_url}")
44
- else:
45
- print(" āŒ MSNBC NOT FOUND.")
46
-
47
- except Exception as e:
48
- print(f" āŒ Error: {e}")
49
-
50
- print("\n=== Benchmark Complete ===")
@@ -1,49 +0,0 @@
1
- import socket
2
- import requests
3
-
4
- host = "zfruvync.duperab.xyz"
5
- print(f"Checking host: {host}")
6
-
7
- # 1. System DNS
8
- try:
9
- ip = socket.gethostbyname(host)
10
- print(f"[System DNS] Resolved to: {ip}")
11
- except Exception as e:
12
- print(f"[System DNS] Failed: {e}")
13
-
14
- # 2. Google DoH
15
- try:
16
- resp = requests.get(f"https://dns.google/resolve?name={host}", timeout=10)
17
- data = resp.json()
18
- if data.get("Status") == 0:
19
- ips = [ans["data"] for ans in data.get("Answer", []) if ans["type"] == 1]
20
- print(f"[Google DoH] Resolved to: {ips}")
21
- else:
22
- print(f"[Google DoH] Failed with status {data.get('Status')}")
23
- except Exception as e:
24
- print(f"[Google DoH] Error: {e}")
25
-
26
- # 3. Cloudflare DoH
27
- try:
28
- resp = requests.get(f"https://cloudflare-dns.com/dns-query?name={host}", headers={"Accept": "application/dns-json"}, timeout=10)
29
- data = resp.json()
30
- if data.get("Status") == 0:
31
- ips = [ans["data"] for ans in data.get("Answer", []) if ans["type"] == 1]
32
- print(f"[Cloudflare DoH] Resolved to: {ips}")
33
- else:
34
- print(f"[Cloudflare DoH] Failed with status {data.get('Status')}")
35
- except Exception as e:
36
- print(f"[Cloudflare DoH] Error: {e}")
37
-
38
- # 4. Quad9 DoH
39
- try:
40
- # Quad9 has a JSON API at https://dns.quad9.net/resolve?name=
41
- resp = requests.get(f"https://dns.quad9.net/resolve?name={host}", timeout=10)
42
- data = resp.json()
43
- if data.get("Status") == 0:
44
- ips = [ans["data"] for ans in data.get("Answer", []) if ans["type"] == 1]
45
- print(f"[Quad9 DoH] Resolved to: {ips}")
46
- else:
47
- print(f"[Quad9 DoH] Failed with status {data.get('Status')}")
48
- except Exception as e:
49
- print(f"[Quad9 DoH] Error: {e}")
@@ -1,133 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const https = require("https");
4
- const os = require("os");
5
-
6
- const binaryName =
7
- os.platform() === "win32" ? "matrix-iptv.exe" : "matrix-iptv";
8
- const binDir = path.join(__dirname, "..", "bin");
9
- const binaryPath = path.join(binDir, binaryName);
10
-
11
- const platformMap = {
12
- win32: "windows.exe",
13
- linux: "linux",
14
- darwin: "macos",
15
- };
16
-
17
- const archMap = {
18
- x64: "x64",
19
- arm64: "arm64",
20
- };
21
-
22
- const platform = platformMap[os.platform()];
23
- if (!platform) {
24
- console.error(`Unsupported platform: ${os.platform()}`);
25
- process.exit(1);
26
- }
27
-
28
- // Note: Re-using the naming convention from install.ps1
29
- // https://github.com/officebeats/matrix-iptv/releases/latest/download/matrix-iptv-windows.exe
30
- const releaseUrl = `https://github.com/officebeats/matrix-iptv/releases/latest/download/matrix-iptv-${platform}`;
31
-
32
- console.log(`[*] Matrix IPTV CLI // One-Click Install`);
33
- console.log(`[*] Platform: ${os.platform()} (${os.arch()})`);
34
- console.log(`[*] Downloading: ${releaseUrl}`);
35
-
36
- if (!fs.existsSync(binDir)) {
37
- fs.mkdirSync(binDir, { recursive: true });
38
- }
39
-
40
- function download(url, dest) {
41
- return new Promise((resolve, reject) => {
42
- https
43
- .get(url, (response) => {
44
- if (response.statusCode === 302 || response.statusCode === 301 || response.statusCode === 307 || response.statusCode === 308) {
45
- // Handle Redirect without leaving locks open
46
- download(response.headers.location, dest).then(resolve).catch(reject);
47
- return;
48
- }
49
-
50
- if (response.statusCode !== 200) {
51
- reject(new Error(`Failed to download: ${response.statusCode}`));
52
- return;
53
- }
54
-
55
- // Only open the file stream if the download actually works
56
- const file = fs.createWriteStream(dest);
57
- response.pipe(file);
58
-
59
- file.on("finish", () => {
60
- file.close();
61
- resolve();
62
- });
63
-
64
- file.on("error", (err) => {
65
- file.close();
66
- fs.unlink(dest, () => {});
67
- reject(err);
68
- });
69
- })
70
- .on("error", (err) => {
71
- reject(err);
72
- });
73
- });
74
- }
75
-
76
- const tempPath = binaryPath + ".tmp";
77
-
78
- download(releaseUrl, tempPath)
79
- .then(async () => {
80
- console.log(`[+] Download complete.`);
81
-
82
- // Replace old binary with robustness logic for Windows
83
- let attempts = 0;
84
- const maxAttempts = 5;
85
- while (attempts < maxAttempts) {
86
- try {
87
- if (fs.existsSync(binaryPath)) {
88
- if (os.platform() === "win32") {
89
- const oldPath = binaryPath + ".old." + Date.now();
90
- fs.renameSync(binaryPath, oldPath);
91
- try {
92
- fs.unlinkSync(oldPath);
93
- } catch (e) {}
94
- } else {
95
- fs.unlinkSync(binaryPath);
96
- }
97
- }
98
- fs.renameSync(tempPath, binaryPath);
99
- break;
100
- } catch (e) {
101
- attempts++;
102
- if (attempts === maxAttempts) throw e;
103
- await new Promise((r) => setTimeout(r, 1000));
104
- }
105
- }
106
-
107
- if (os.platform() !== "win32") {
108
- fs.chmodSync(binaryPath, "755");
109
- console.log(`[+] Executable permissions set.`);
110
- }
111
-
112
- console.log(`\nāœ… Matrix IPTV CLI installed successfully.`);
113
-
114
- // Auto-launch if running in an interactive terminal
115
- if (process.stdout.isTTY) {
116
- console.log(`šŸš€ Launching Matrix IPTV...`);
117
- const { spawn } = require("child_process");
118
- const child = spawn(binaryPath, [], { stdio: "inherit" });
119
-
120
- child.on("close", (code) => {
121
- process.exit(code);
122
- });
123
- } else {
124
- console.log(`Type 'matrix-iptv' to start.`);
125
- }
126
- })
127
- .catch((err) => {
128
- console.error(`\nāŒ Installation failed: ${err.message}`);
129
- console.log(
130
- `Please ensure the GitHub repository is public and has a 'latest' release.`
131
- );
132
- process.exit(1);
133
- });