Compare commits
No commits in common. "master" and "v0.0.11" have entirely different histories.
@ -44,9 +44,6 @@ bun run build
|
|||||||
|
|
||||||
# Step 2: Build Android APK
|
# Step 2: Build Android APK
|
||||||
if [ "$BUILD_TYPE" = "release" ]; then
|
if [ "$BUILD_TYPE" = "release" ]; then
|
||||||
# Configure release signing from .env (single source of truth). Must run
|
|
||||||
# after sync-android-sources.sh, since gen/android is (re)generated there.
|
|
||||||
./scripts/write-keystore-properties.sh
|
|
||||||
echo "📦 Building release APK..."
|
echo "📦 Building release APK..."
|
||||||
bun run tauri android build --apk true
|
bun run tauri android build --apk true
|
||||||
else
|
else
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Regenerate src-tauri/gen/android/keystore.properties from the gitignored .env.
|
|
||||||
#
|
|
||||||
# .env is the single source of truth for local release signing. `tauri android
|
|
||||||
# init` wipes/regenerates gen/android, so keystore.properties must be rewritten
|
|
||||||
# from .env before every release build (this is the local mirror of what the CI
|
|
||||||
# workflow does from Gitea secrets).
|
|
||||||
#
|
|
||||||
# Required .env vars:
|
|
||||||
# ANDROID_KEY_ALIAS, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_PASSWORD,
|
|
||||||
# ANDROID_KEYSTORE_FILE (absolute path to the .jks)
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
||||||
ENV_FILE="$PROJECT_ROOT/.env"
|
|
||||||
PROPS="$PROJECT_ROOT/src-tauri/gen/android/keystore.properties"
|
|
||||||
|
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
|
||||||
echo "❌ $ENV_FILE not found — cannot configure release signing." >&2
|
|
||||||
echo " Create it with ANDROID_KEY_ALIAS / ANDROID_KEYSTORE_PASSWORD /" >&2
|
|
||||||
echo " ANDROID_KEY_PASSWORD / ANDROID_KEYSTORE_FILE." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Load .env without leaking it into the caller's environment beyond what we need.
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "$ENV_FILE"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
: "${ANDROID_KEY_ALIAS:?ANDROID_KEY_ALIAS missing from .env}"
|
|
||||||
: "${ANDROID_KEYSTORE_PASSWORD:?ANDROID_KEYSTORE_PASSWORD missing from .env}"
|
|
||||||
: "${ANDROID_KEY_PASSWORD:?ANDROID_KEY_PASSWORD missing from .env}"
|
|
||||||
: "${ANDROID_KEYSTORE_FILE:?ANDROID_KEYSTORE_FILE missing from .env}"
|
|
||||||
|
|
||||||
if [ ! -f "$ANDROID_KEYSTORE_FILE" ]; then
|
|
||||||
echo "❌ Keystore not found at ANDROID_KEYSTORE_FILE=$ANDROID_KEYSTORE_FILE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "$PROPS")"
|
|
||||||
umask 077
|
|
||||||
cat > "$PROPS" <<EOF
|
|
||||||
storeFile=$ANDROID_KEYSTORE_FILE
|
|
||||||
storePassword=$ANDROID_KEYSTORE_PASSWORD
|
|
||||||
keyAlias=$ANDROID_KEY_ALIAS
|
|
||||||
keyPassword=$ANDROID_KEY_PASSWORD
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "🔐 Wrote release signing config to keystore.properties (from .env)"
|
|
||||||
@ -133,7 +133,7 @@ impl AuthManager {
|
|||||||
|
|
||||||
log::info!("[AuthManager] Connecting to server: {}", normalized_url);
|
log::info!("[AuthManager] Connecting to server: {}", normalized_url);
|
||||||
|
|
||||||
match self.http_client.get_json_fast::<PublicSystemInfo>(&endpoint).await {
|
match self.http_client.get_json_with_retry::<PublicSystemInfo>(&endpoint).await {
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
log::info!("[AuthManager] Connected to server: {} ({})", info.server_name, info.version);
|
log::info!("[AuthManager] Connected to server: {} ({})", info.server_name, info.version);
|
||||||
|
|
||||||
|
|||||||
@ -271,21 +271,6 @@ pub async fn resume_queued_downloads(
|
|||||||
(Arc::new(database.service()), target_dir)
|
(Arc::new(database.service()), target_dir)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Recover stale downloads: rows left in 'downloading' when the app was killed
|
|
||||||
// mid-transfer are orphaned — nothing ever restarts them, so they show as
|
|
||||||
// permanently "downloading". Reset them to 'pending' and clear the stale
|
|
||||||
// stream_url so they get re-resolved and restarted from scratch below.
|
|
||||||
let recover_query = Query::new(
|
|
||||||
"UPDATE downloads SET status = 'pending', stream_url = NULL, progress = 0, \
|
|
||||||
bytes_downloaded = 0, started_at = NULL \
|
|
||||||
WHERE status = 'downloading'",
|
|
||||||
);
|
|
||||||
match db_service.execute(recover_query).await {
|
|
||||||
Ok(n) if n > 0 => info!("[Catalog] Reset {} stale 'downloading' rows to pending", n),
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => warn!("[Catalog] Failed to reset stale downloads: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve each row's URL against the (now reachable) repository.
|
// Resolve each row's URL against the (now reachable) repository.
|
||||||
let repo_for_resolve = Arc::clone(&repo);
|
let repo_for_resolve = Arc::clone(&repo);
|
||||||
let outcome = resolve_pending_download_urls(
|
let outcome = resolve_pending_download_urls(
|
||||||
|
|||||||
@ -217,45 +217,6 @@ impl HttpClient {
|
|||||||
.map_err(|e| format!("Failed to parse JSON: {}", e))
|
.map_err(|e| format!("Failed to parse JSON: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make a GET request and deserialize JSON with a short timeout and no retries.
|
|
||||||
///
|
|
||||||
/// Intended for the initial "connect to server" probe on the login screen:
|
|
||||||
/// a wrong/unreachable URL must fail fast instead of burning through the
|
|
||||||
/// default 30s-per-attempt timeout and exponential backoff retries.
|
|
||||||
pub async fn get_json_fast<T: DeserializeOwned>(&self, url: &str) -> Result<T, String> {
|
|
||||||
// Short timeout so an unreachable host fails quickly.
|
|
||||||
const FAST_TIMEOUT: Duration = Duration::from_secs(10);
|
|
||||||
|
|
||||||
let request = self
|
|
||||||
.client
|
|
||||||
.get(url)
|
|
||||||
.timeout(FAST_TIMEOUT)
|
|
||||||
.build()
|
|
||||||
.map_err(|e| format!("Failed to build request: {}", e))?;
|
|
||||||
|
|
||||||
// No retry: connection failures on a wrong URL won't succeed on retry,
|
|
||||||
// they'd only multiply the wait the user sees before an error.
|
|
||||||
let response = self
|
|
||||||
.client
|
|
||||||
.execute(request)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Request failed: {}", e))?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
let status = response.status();
|
|
||||||
let error_text = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
|
||||||
return Err(format!("HTTP {}: {}", status, error_text));
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
.json::<T>()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to parse JSON: {}", e))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Quick ping to check if a server is reachable (no retry)
|
/// Quick ping to check if a server is reachable (no retry)
|
||||||
pub async fn ping(&self, url: &str) -> bool {
|
pub async fn ping(&self, url: &str) -> bool {
|
||||||
let request = self.client.get(url)
|
let request = self.client.get(url)
|
||||||
|
|||||||
@ -934,11 +934,9 @@ pub fn run() {
|
|||||||
playback_mode_arc.clone(),
|
playback_mode_arc.clone(),
|
||||||
);
|
);
|
||||||
session_poller.set_event_emitter(event_emitter.clone());
|
session_poller.set_event_emitter(event_emitter.clone());
|
||||||
// Note: start() is deferred until after the connectivity monitor is
|
session_poller.start();
|
||||||
// created below, so the poller can report reachability from its first
|
|
||||||
// poll (it drives offline detection + recovery while the user is idle).
|
|
||||||
let session_poller_arc = Arc::new(session_poller);
|
let session_poller_arc = Arc::new(session_poller);
|
||||||
let session_poller_wrapper = SessionPollerWrapper(session_poller_arc.clone());
|
let session_poller_wrapper = SessionPollerWrapper(session_poller_arc);
|
||||||
app.manage(session_poller_wrapper);
|
app.manage(session_poller_wrapper);
|
||||||
|
|
||||||
// On Android, set up the MediaSession (lockscreen) handler and the
|
// On Android, set up the MediaSession (lockscreen) handler and the
|
||||||
@ -1000,12 +998,6 @@ pub fn run() {
|
|||||||
let mut connectivity_monitor = ConnectivityMonitor::new(http_client);
|
let mut connectivity_monitor = ConnectivityMonitor::new(http_client);
|
||||||
connectivity_monitor.set_app_handle(app.handle().clone());
|
connectivity_monitor.set_app_handle(app.handle().clone());
|
||||||
|
|
||||||
// Wire the connectivity reporter into the session poller so its
|
|
||||||
// continuous background polls drive reachability (offline detection
|
|
||||||
// + recovery) even when the user isn't browsing, then start it.
|
|
||||||
session_poller_arc.set_connectivity_reporter(connectivity_monitor.reporter());
|
|
||||||
session_poller_arc.start();
|
|
||||||
|
|
||||||
// Wrap in Arc for sharing with AuthManager
|
// Wrap in Arc for sharing with AuthManager
|
||||||
let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
|
let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
|
||||||
let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
|
let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
|
||||||
|
|||||||
@ -32,13 +32,6 @@ pub struct SessionPollerManager {
|
|||||||
jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
|
jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
|
||||||
playback_mode_manager: Arc<PlaybackModeManager>,
|
playback_mode_manager: Arc<PlaybackModeManager>,
|
||||||
event_emitter: Arc<Mutex<Option<Arc<dyn PlayerEventEmitter>>>>,
|
event_emitter: Arc<Mutex<Option<Arc<dyn PlayerEventEmitter>>>>,
|
||||||
/// Optional connectivity reporter. The session poller is the one piece of
|
|
||||||
/// server traffic that runs continuously even when the user is idle (not
|
|
||||||
/// browsing the library), so feeding its poll outcomes into the reporter is
|
|
||||||
/// what lets the app detect going offline — and, crucially, recover when the
|
|
||||||
/// server returns — without any user interaction. Repository traffic alone
|
|
||||||
/// can't do this because it only happens while browsing.
|
|
||||||
connectivity_reporter: Arc<Mutex<Option<crate::connectivity::ConnectivityReporter>>>,
|
|
||||||
|
|
||||||
// Polling state
|
// Polling state
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
@ -59,7 +52,6 @@ impl SessionPollerManager {
|
|||||||
jellyfin_client,
|
jellyfin_client,
|
||||||
playback_mode_manager,
|
playback_mode_manager,
|
||||||
event_emitter: Arc::new(Mutex::new(None)),
|
event_emitter: Arc::new(Mutex::new(None)),
|
||||||
connectivity_reporter: Arc::new(Mutex::new(None)),
|
|
||||||
is_running: Arc::new(AtomicBool::new(false)),
|
is_running: Arc::new(AtomicBool::new(false)),
|
||||||
current_hint: Arc::new(RwLock::new(PollingHint::Normal)),
|
current_hint: Arc::new(RwLock::new(PollingHint::Normal)),
|
||||||
current_interval_ms: Arc::new(AtomicU64::new(10000)), // Default 10s
|
current_interval_ms: Arc::new(AtomicU64::new(10000)), // Default 10s
|
||||||
@ -72,13 +64,6 @@ impl SessionPollerManager {
|
|||||||
*self.event_emitter.lock_safe() = Some(emitter);
|
*self.event_emitter.lock_safe() = Some(emitter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wire the connectivity reporter so each poll outcome updates reachability.
|
|
||||||
/// A successful poll recovers the app to online instantly; sustained poll
|
|
||||||
/// failures flip it offline (subject to the reporter's debounce window).
|
|
||||||
pub fn set_connectivity_reporter(&self, reporter: crate::connectivity::ConnectivityReporter) {
|
|
||||||
*self.connectivity_reporter.lock_safe() = Some(reporter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start the background polling thread
|
/// Start the background polling thread
|
||||||
pub fn start(&self) {
|
pub fn start(&self) {
|
||||||
if self.is_running.swap(true, Ordering::Relaxed) {
|
if self.is_running.swap(true, Ordering::Relaxed) {
|
||||||
@ -92,7 +77,6 @@ impl SessionPollerManager {
|
|||||||
let client = self.jellyfin_client.clone();
|
let client = self.jellyfin_client.clone();
|
||||||
let mode_manager = self.playback_mode_manager.clone();
|
let mode_manager = self.playback_mode_manager.clone();
|
||||||
let emitter = self.event_emitter.clone();
|
let emitter = self.event_emitter.clone();
|
||||||
let connectivity_reporter = self.connectivity_reporter.clone();
|
|
||||||
let is_running = self.is_running.clone();
|
let is_running = self.is_running.clone();
|
||||||
let hint = self.current_hint.clone();
|
let hint = self.current_hint.clone();
|
||||||
let interval_ms = self.current_interval_ms.clone();
|
let interval_ms = self.current_interval_ms.clone();
|
||||||
@ -112,35 +96,18 @@ impl SessionPollerManager {
|
|||||||
|
|
||||||
debug!("[SessionPoller] Polling with interval: {}ms", new_interval);
|
debug!("[SessionPoller] Polling with interval: {}ms", new_interval);
|
||||||
|
|
||||||
// Fetch sessions. `had_client` distinguishes "server didn't
|
// Fetch sessions
|
||||||
// answer" from "no client configured" so we only feed real
|
let sessions_result = rt.block_on(async {
|
||||||
// request outcomes into the connectivity reporter.
|
|
||||||
let (sessions_result, had_client) = rt.block_on(async {
|
|
||||||
let client_opt = client.lock_safe().clone();
|
let client_opt = client.lock_safe().clone();
|
||||||
match client_opt {
|
match client_opt {
|
||||||
Some(c) => (c.get_sessions().await, true),
|
Some(c) => c.get_sessions().await,
|
||||||
None => {
|
None => {
|
||||||
debug!("[SessionPoller] Jellyfin client not configured, skipping poll");
|
debug!("[SessionPoller] Jellyfin client not configured, skipping poll");
|
||||||
(Ok(Vec::new()), false)
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Drive the connectivity reporter from this poll's outcome. This
|
|
||||||
// is what recovers the app to online when the server returns
|
|
||||||
// while the user is idle, and detects going offline when no
|
|
||||||
// library browsing is happening. See connectivity/mod.rs.
|
|
||||||
if had_client {
|
|
||||||
if let Some(reporter) = connectivity_reporter.lock_safe().clone() {
|
|
||||||
rt.block_on(async {
|
|
||||||
match &sessions_result {
|
|
||||||
Ok(_) => reporter.report_success().await,
|
|
||||||
Err(e) => reporter.report_network_failure(Some(e.clone())).await,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit event if successful
|
// Emit event if successful
|
||||||
match sessions_result {
|
match sessions_result {
|
||||||
Ok(sessions) => {
|
Ok(sessions) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user