mirror of
https://gitlab.com/vyla-entertainment/go.git
synced 2026-11-14 07:27:23 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 084d215b23 | |||
| a4b3a77153 | |||
|
|
9b9d0dfc49 | ||
|
|
7f7a969698 | ||
|
|
fd738eeba5 |
@@ -64,4 +64,9 @@ Force-stop and clear app data:
|
||||
```powershell
|
||||
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell am force-stop com.vyla.go
|
||||
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell pm clear com.vyla.go
|
||||
```
|
||||
```
|
||||
|
||||
Fix emulator DNS timeouts (if emulator gets network timeouts connecting to GitLab):
|
||||
```powershell
|
||||
& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" shell settings put global private_dns_mode off
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vyla-go",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.12",
|
||||
"description": "Vyla Go Mobile Launcher",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -23,4 +23,4 @@
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -4593,6 +4593,7 @@ dependencies = [
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tokio",
|
||||
"urlencoding",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -23,3 +23,4 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"
|
||||
zip = { version = "2.2", default-features = false, features = ["deflate"] }
|
||||
futures-util = "0.3"
|
||||
urlencoding = "2.1"
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
|
||||
2875
src-tauri/gen/schemas/desktop-schema.json
Normal file
2875
src-tauri/gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
2875
src-tauri/gen/schemas/windows-schema.json
Normal file
2875
src-tauri/gen/schemas/windows-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,113 @@ const RUNTIME_INTERCEPT_SCRIPT: &str = r#"
|
||||
})();
|
||||
"#;
|
||||
|
||||
const NODE_PRELOAD_SCRIPT: &str = r#"
|
||||
const Module = require('module');
|
||||
const origLoad = Module._load;
|
||||
|
||||
const stub = new Proxy(function () {}, {
|
||||
get: (t, k) => (typeof k === 'symbol' || k === 'then' ? undefined : stub),
|
||||
apply: () => stub,
|
||||
construct: () => stub,
|
||||
});
|
||||
|
||||
function buildTlsShim() {
|
||||
class Session {
|
||||
constructor(opts) {
|
||||
this.opts = opts || {};
|
||||
this.headers = this.opts.headers || {};
|
||||
}
|
||||
async request(method, url, options) {
|
||||
options = options || {};
|
||||
const headers = Object.assign({}, this.headers, options.headers || {});
|
||||
const init = {
|
||||
method,
|
||||
headers,
|
||||
redirect: options.followRedirects === false ? 'manual' : 'follow',
|
||||
};
|
||||
let body = options.body !== undefined ? options.body : options.data;
|
||||
if (options.json !== undefined) {
|
||||
body = JSON.stringify(options.json);
|
||||
if (!headers['content-type'] && !headers['Content-Type']) {
|
||||
headers['content-type'] = 'application/json';
|
||||
}
|
||||
} else if (body !== undefined && body !== null && typeof body === 'object' && !Buffer.isBuffer(body) && !(body instanceof URLSearchParams)) {
|
||||
body = new URLSearchParams(body).toString();
|
||||
if (!headers['content-type'] && !headers['Content-Type']) {
|
||||
headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
}
|
||||
if (body !== undefined && body !== null && method !== 'GET' && method !== 'HEAD') {
|
||||
init.body = body;
|
||||
}
|
||||
let ms = Number(options.timeout || this.opts.timeout || 30000);
|
||||
if (ms < 1000) ms = ms * 1000;
|
||||
init.signal = AbortSignal.timeout(ms);
|
||||
const res = await fetch(url, init);
|
||||
const text = await res.text();
|
||||
const outHeaders = {};
|
||||
res.headers.forEach((v, k) => { outHeaders[k] = v; });
|
||||
return {
|
||||
id: String(Date.now()),
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
success: res.ok,
|
||||
url: res.url,
|
||||
target: res.url,
|
||||
headers: outHeaders,
|
||||
cookies: {},
|
||||
body: text,
|
||||
text: async () => text,
|
||||
json: async () => JSON.parse(text),
|
||||
};
|
||||
}
|
||||
get(url, o) { return this.request('GET', url, o); }
|
||||
post(url, o) { return this.request('POST', url, o); }
|
||||
put(url, o) { return this.request('PUT', url, o); }
|
||||
patch(url, o) { return this.request('PATCH', url, o); }
|
||||
delete(url, o) { return this.request('DELETE', url, o); }
|
||||
head(url, o) { return this.request('HEAD', url, o); }
|
||||
options(url, o) { return this.request('OPTIONS', url, o); }
|
||||
async close() {}
|
||||
async free() {}
|
||||
}
|
||||
const ClientIdentifier = new Proxy({}, { get: (t, k) => (typeof k === 'symbol' ? undefined : String(k)) });
|
||||
const mod = {
|
||||
Session,
|
||||
ClientIdentifier,
|
||||
initTLS: async () => {},
|
||||
destroyTLS: async () => {},
|
||||
};
|
||||
mod.default = mod;
|
||||
return mod;
|
||||
}
|
||||
|
||||
let tlsShim = null;
|
||||
|
||||
Module._load = function (request) {
|
||||
if (request === 'node-tls-client') {
|
||||
if (!tlsShim) {
|
||||
tlsShim = buildTlsShim();
|
||||
console.error('node-tls-client replaced with fetch shim');
|
||||
}
|
||||
return tlsShim;
|
||||
}
|
||||
try {
|
||||
return origLoad.apply(this, arguments);
|
||||
} catch (e) {
|
||||
const msg = String(e && e.message);
|
||||
if (/koffi/i.test(request) || /koffi/i.test(msg)) {
|
||||
console.error('stubbed native module: ' + request + ' (' + msg + ')');
|
||||
return stub;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
process.on('uncaughtException', (e) => console.error('uncaughtException', e && e.stack || e));
|
||||
process.on('unhandledRejection', (e) => console.error('unhandledRejection', e && e.stack || e));
|
||||
"#;
|
||||
|
||||
fn android_log(msg: &str) {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
@@ -135,6 +242,13 @@ fn spawn_service_manager(base_dir: &std::path::Path) {
|
||||
.map(|d| d.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let tmp_dir = base_dir.join("tmp");
|
||||
let _ = std::fs::create_dir_all(&tmp_dir);
|
||||
std::env::set_var("HOME", base_dir.to_string_lossy().to_string());
|
||||
std::env::set_var("TMPDIR", tmp_dir.to_string_lossy().to_string());
|
||||
let preload_path = base_dir.join("preload.js");
|
||||
let _ = std::fs::write(&preload_path, NODE_PRELOAD_SCRIPT);
|
||||
let preload_str = preload_path.to_string_lossy().to_string();
|
||||
std::env::set_var("PORT", "3000");
|
||||
std::env::set_var("NODE_ENV", "production");
|
||||
std::env::set_var("TMDB_API_KEY", "3f6703f3e2a0b2b40fe4fbb3e1546244");
|
||||
@@ -150,26 +264,33 @@ fn spawn_service_manager(base_dir: &std::path::Path) {
|
||||
let server_js = server_script.to_string_lossy().to_string();
|
||||
let player_dir_str = player_dir.to_string_lossy().to_string();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let _ = std::thread::Builder::new().stack_size(16 * 1024 * 1024).spawn(move || {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let _ = std::env::set_current_dir(&player_dir_str);
|
||||
|
||||
let arg0 = std::ffi::CString::new("node").unwrap();
|
||||
let arg1 = std::ffi::CString::new(server_js.as_str()).unwrap();
|
||||
let parts: [&str; 4] = ["node", "--require", preload_str.as_str(), server_js.as_str()];
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
let mut offsets: Vec<usize> = Vec::new();
|
||||
for p in parts.iter() {
|
||||
offsets.push(buf.len());
|
||||
buf.extend_from_slice(p.as_bytes());
|
||||
buf.push(0);
|
||||
}
|
||||
buf.shrink_to_fit();
|
||||
|
||||
let mut argv: Vec<*mut std::os::raw::c_char> = vec![
|
||||
arg0.as_ptr() as *mut _,
|
||||
arg1.as_ptr() as *mut _,
|
||||
std::ptr::null_mut(),
|
||||
];
|
||||
let base = buf.as_mut_ptr() as *mut std::os::raw::c_char;
|
||||
let mut argv: Vec<*mut std::os::raw::c_char> = offsets
|
||||
.iter()
|
||||
.map(|o| unsafe { base.add(*o) })
|
||||
.collect();
|
||||
argv.push(std::ptr::null_mut());
|
||||
|
||||
std::mem::forget(buf);
|
||||
|
||||
android_log("calling node::Start via FFI");
|
||||
let ret = unsafe { node_ffi::_ZN4node5StartEiPPc(2, argv.as_mut_ptr()) };
|
||||
let ret = unsafe { node_ffi::_ZN4node5StartEiPPc(4, argv.as_mut_ptr()) };
|
||||
android_log(&format!("node::Start returned: {}", ret));
|
||||
|
||||
drop(arg0);
|
||||
drop(arg1);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -231,44 +352,151 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind
|
||||
let base_dir = get_app_dir(&app);
|
||||
std::fs::create_dir_all(&base_dir).map_err(|e| format!("Storage dir error: {}", e))?;
|
||||
|
||||
let marker = base_dir.join(".installed");
|
||||
let frontend_index = base_dir.join("frontend").join("index.html");
|
||||
if frontend_index.exists() {
|
||||
let server_script = base_dir.join("player").join("server.js");
|
||||
if marker.exists() && frontend_index.exists() && server_script.exists() {
|
||||
android_log("App already installed, launching...");
|
||||
apply_patches(&base_dir);
|
||||
spawn_service_manager(&base_dir);
|
||||
update_ui(&window, "Launching Vyla Go...", Some(100));
|
||||
return Ok(base_dir);
|
||||
}
|
||||
|
||||
update_ui(&window, "Connecting to GitLab...", Some(10));
|
||||
// 1. Check if a valid local archive already exists on device
|
||||
let local_payload_candidates = [
|
||||
base_dir.join("update-payload.zip"),
|
||||
base_dir.join("temp-payload.zip"),
|
||||
PathBuf::from("/sdcard/Download/update-payload.zip"),
|
||||
PathBuf::from("/data/local/tmp/update-payload.zip"),
|
||||
];
|
||||
|
||||
for candidate in &local_payload_candidates {
|
||||
if candidate.exists() {
|
||||
if let Ok(meta) = std::fs::metadata(candidate) {
|
||||
if meta.len() > 10_000_000 {
|
||||
if let Ok(f) = File::open(candidate) {
|
||||
if let Ok(archive) = zip::ZipArchive::new(f) {
|
||||
if archive.len() > 0 {
|
||||
android_log(&format!("Found valid local payload: {}", candidate.display()));
|
||||
update_ui(&window, "Unpacking local package...", Some(80));
|
||||
let extract_dir = base_dir.clone();
|
||||
let candidate_path = candidate.clone();
|
||||
let extract_ok = tauri::async_runtime::spawn_blocking(move || -> Result<(), String> {
|
||||
let zip_file = File::open(&candidate_path).map_err(|e| format!("Open zip error: {}", e))?;
|
||||
let mut archive = zip::ZipArchive::new(zip_file).map_err(|e| format!("Zip parse error: {}", e))?;
|
||||
archive.extract(&extract_dir).map_err(|e| format!("Extraction error: {}", e))?;
|
||||
let _ = std::fs::write(extract_dir.join(".installed"), "ok");
|
||||
apply_patches(&extract_dir);
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if extract_ok.is_ok() {
|
||||
spawn_service_manager(&base_dir);
|
||||
update_ui(&window, "Launching Vyla Go...", Some(100));
|
||||
return Ok(base_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("Mozilla/5.0 (Android) VylaGo/1.0")
|
||||
.connect_timeout(std::time::Duration::from_secs(30))
|
||||
.connect_timeout(std::time::Duration::from_secs(15))
|
||||
.timeout(std::time::Duration::from_secs(180))
|
||||
.redirect(reqwest::redirect::Policy::limited(10))
|
||||
.build()
|
||||
.map_err(|e| format!("Client error: {}", e))?;
|
||||
|
||||
let releases_url = "https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases";
|
||||
let res = client.get(releases_url).send().await.map_err(|e| format!("Release check failed: {}", e))?;
|
||||
let json: serde_json::Value = res.json().await.map_err(|e| format!("JSON error: {}", e))?;
|
||||
let release_endpoints = [
|
||||
"https://gitlab.com/api/v4/projects/vyla-entertainment%2Fgo/releases",
|
||||
"https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases",
|
||||
];
|
||||
|
||||
let download_url = json.get(0)
|
||||
.and_then(|r| r.get("assets"))
|
||||
.and_then(|a| a.get("links"))
|
||||
.and_then(|l| l.as_array())
|
||||
.and_then(|links| {
|
||||
links.iter().find(|item| {
|
||||
item.get("name").and_then(|n| n.as_str()) == Some("update-payload.zip")
|
||||
})
|
||||
})
|
||||
.and_then(|asset| asset.get("url"))
|
||||
.and_then(|u| u.as_str())
|
||||
.ok_or_else(|| "Could not locate update-payload.zip link in release assets".to_string())?;
|
||||
let mut download_url: Option<String> = None;
|
||||
let mut last_error = String::new();
|
||||
|
||||
const MAX_RETRIES: u32 = 4;
|
||||
for attempt in 1..=MAX_RETRIES {
|
||||
let msg = if attempt == 1 {
|
||||
"Connecting to GitLab...".to_string()
|
||||
} else {
|
||||
format!("Connecting to GitLab (attempt {}/{})...", attempt, MAX_RETRIES)
|
||||
};
|
||||
update_ui(&window, &msg, Some(10 + (attempt - 1) * 3));
|
||||
|
||||
for endpoint in &release_endpoints {
|
||||
android_log(&format!("Checking endpoint: {} (attempt {})", endpoint, attempt));
|
||||
match client.get(*endpoint).send().await {
|
||||
Ok(res) => {
|
||||
if res.status().is_success() {
|
||||
if let Ok(json) = res.json::<serde_json::Value>().await {
|
||||
if let Some(releases) = json.as_array() {
|
||||
for rel in releases {
|
||||
if let Some(links) = rel.get("assets").and_then(|a| a.get("links")).and_then(|l| l.as_array()) {
|
||||
for item in links {
|
||||
if item.get("name").and_then(|n| n.as_str()) == Some("update-payload.zip") {
|
||||
if let Some(url) = item.get("url").and_then(|u| u.as_str()) {
|
||||
download_url = Some(url.to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if download_url.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
last_error = format!("HTTP {}", res.status());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
android_log(&format!("Release check failed for {}: {}", endpoint, e));
|
||||
last_error = format!("{}", e);
|
||||
}
|
||||
}
|
||||
|
||||
if download_url.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if download_url.is_some() {
|
||||
break;
|
||||
}
|
||||
|
||||
if attempt < MAX_RETRIES {
|
||||
let backoff = attempt * 2;
|
||||
update_ui(
|
||||
&window,
|
||||
&format!("Connection pending, retrying in {}s...", backoff),
|
||||
Some(10 + attempt * 3),
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(backoff as u64)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let download_url = download_url.ok_or_else(|| {
|
||||
format!(
|
||||
"Could not connect to release server ({})",
|
||||
if last_error.is_empty() { "timeout" } else { &last_error }
|
||||
)
|
||||
})?;
|
||||
|
||||
update_ui(&window, "Downloading update payload...", Some(25));
|
||||
|
||||
android_log(&format!("Downloading from: {}", download_url));
|
||||
let response = client.get(download_url).send().await.map_err(|e| format!("Download error: {} (source: {:?})", e, StdError::source(&e)))?; android_log(&format!("Response status: {}", response.status()));
|
||||
let response = client.get(&download_url).send().await.map_err(|e| format!("Download error: {} (source: {:?})", e, StdError::source(&e)))?;
|
||||
android_log(&format!("Response status: {}", response.status()));
|
||||
let total_size = response.content_length().unwrap_or(0) as usize;
|
||||
|
||||
let temp_zip_path = base_dir.join("temp-payload.zip");
|
||||
@@ -287,7 +515,7 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind
|
||||
downloaded += chunk.len();
|
||||
|
||||
let percent = if total_size > 0 {
|
||||
10 + ((downloaded as f64 / total_size as f64) * 75.0) as u32
|
||||
25 + ((downloaded as f64 / total_size as f64) * 65.0) as u32
|
||||
} else {
|
||||
35
|
||||
};
|
||||
@@ -310,7 +538,8 @@ async fn download_and_extract_payload(app: tauri::AppHandle, window: WebviewWind
|
||||
let zip_file = File::open(&zip_path_clone).map_err(|e| format!("Open zip error: {}", e))?;
|
||||
let mut archive = zip::ZipArchive::new(zip_file).map_err(|e| format!("Zip parse error: {}", e))?;
|
||||
archive.extract(&extract_dir).map_err(|e| format!("Extraction error: {}", e))?;
|
||||
let _ = std::fs::remove_file(zip_path_clone);
|
||||
let _ = std::fs::remove_file(&zip_path_clone);
|
||||
let _ = std::fs::write(extract_dir.join(".installed"), "ok");
|
||||
apply_patches(&extract_dir);
|
||||
Ok(())
|
||||
})
|
||||
@@ -433,25 +662,32 @@ pub fn run() {
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
std::thread::sleep(std::time::Duration::from_millis(400));
|
||||
std::thread::sleep(std::time::Duration::from_millis(600));
|
||||
|
||||
if let Some(window) = handle.get_webview_window("main") {
|
||||
update_ui(&window, "Initializing Vyla Go engine...", Some(5));
|
||||
|
||||
match download_and_extract_payload(handle.clone(), window.clone()).await {
|
||||
Ok(_) => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(400));
|
||||
let _ = window.eval(&format!(
|
||||
"{}; window.location.replace('http://vyla.localhost/frontend/index.html');",
|
||||
ANDROID_VIDEO_FIX_SCRIPT
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
update_ui(
|
||||
&window,
|
||||
&format!("Error: {}", err),
|
||||
Some(0),
|
||||
);
|
||||
loop {
|
||||
match download_and_extract_payload(handle.clone(), window.clone()).await {
|
||||
Ok(_) => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(400));
|
||||
let _ = window.eval(&format!(
|
||||
"{}; window.location.replace('http://vyla.localhost/frontend/index.html');",
|
||||
ANDROID_VIDEO_FIX_SCRIPT
|
||||
));
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
android_log(&format!("Installer error: {}", err));
|
||||
for remaining in (1..=8).rev() {
|
||||
update_ui(
|
||||
&window,
|
||||
&format!("Error: {}. Retrying in {}s...", err, remaining),
|
||||
Some(0),
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Vyla Go",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.12",
|
||||
"identifier": "com.vyla.go",
|
||||
"build": {
|
||||
"beforeDevCommand": "",
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
</div>
|
||||
|
||||
<div class="center-brand">
|
||||
<img src="https://gitlab.com/vyla-entertainment/home-frontend/-/raw/main/assets/images/logo.png" alt="Logo" />
|
||||
<img src="https://gitlab.com/vyla-entertainment/go/-/raw/main/assets/logos/white_logo.png" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<div class="bottom-dock">
|
||||
|
||||
Reference in New Issue
Block a user