mirror of
https://github.com/NuvioMedia/NuvioMobile.git
synced 2026-07-27 01:22:18 +00:00
Merge branch 'toropt' into cmp-rewrite
This commit is contained in:
commit
dcf4d059f6
17 changed files with 1031 additions and 212 deletions
|
|
@ -352,6 +352,14 @@ android {
|
|||
manifest.srcFile("src/androidFull/AndroidManifest.xml")
|
||||
java.srcDir(fullCommonSourceDir)
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -1697,7 +1697,7 @@ private fun MainAppContent(
|
|||
replaceStreamRoute: Boolean,
|
||||
) {
|
||||
val infoHash = stream.p2pInfoHash ?: return
|
||||
val sentinelUrl = p2pSentinelUrl(infoHash, stream.fileIdx)
|
||||
val sentinelUrl = p2pSentinelUrl(infoHash, stream.p2pFileIdx)
|
||||
if (playerSettings.streamReuseLastLinkEnabled) {
|
||||
val cacheKey = StreamLinkCacheRepository.contentKey(
|
||||
type = launch.type,
|
||||
|
|
@ -1714,11 +1714,12 @@ private fun MainAppContent(
|
|||
addonId = stream.addonId,
|
||||
requestHeaders = emptyMap(),
|
||||
responseHeaders = emptyMap(),
|
||||
filename = stream.behaviorHints.filename,
|
||||
filename = stream.p2pFilename,
|
||||
videoSize = stream.behaviorHints.videoSize,
|
||||
infoHash = infoHash,
|
||||
fileIdx = stream.fileIdx,
|
||||
sources = stream.sources,
|
||||
fileIdx = stream.p2pFileIdx,
|
||||
magnetUri = stream.torrentMagnetUri,
|
||||
sources = stream.p2pSourceHints,
|
||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||
)
|
||||
}
|
||||
|
|
@ -1745,8 +1746,9 @@ private fun MainAppContent(
|
|||
parentMetaId = launch.parentMetaId ?: effectiveVideoId,
|
||||
parentMetaType = launch.parentMetaType ?: launch.type,
|
||||
torrentInfoHash = infoHash,
|
||||
torrentFileIdx = stream.fileIdx,
|
||||
torrentFilename = stream.behaviorHints.filename,
|
||||
torrentFileIdx = stream.p2pFileIdx,
|
||||
torrentFilename = stream.p2pFilename,
|
||||
torrentMagnetUri = stream.torrentMagnetUri,
|
||||
torrentTrackers = stream.p2pTrackers,
|
||||
initialPositionMs = resolvedResumePositionMs ?: 0L,
|
||||
initialProgressFraction = resolvedResumeProgressFraction,
|
||||
|
|
@ -1814,10 +1816,10 @@ private fun MainAppContent(
|
|||
val maxAgeMs = playerSettings.streamReuseLastLinkCacheHours * 60L * 60L * 1000L
|
||||
val cached = StreamLinkCacheRepository.getValid(cacheKey, maxAgeMs)
|
||||
if (cached != null) {
|
||||
if (cached.url.isBlank() && !cached.infoHash.isNullOrBlank()) {
|
||||
if (cached.url.isBlank() && (!cached.infoHash.isNullOrBlank() || !cached.magnetUri.isNullOrBlank())) {
|
||||
val cachedStream = StreamItem(
|
||||
name = cached.streamName,
|
||||
url = null,
|
||||
url = cached.magnetUri,
|
||||
infoHash = cached.infoHash,
|
||||
fileIdx = cached.fileIdx,
|
||||
sources = cached.sources,
|
||||
|
|
@ -2329,6 +2331,7 @@ private fun MainAppContent(
|
|||
torrentInfoHash = launch.torrentInfoHash,
|
||||
torrentFileIdx = launch.torrentFileIdx,
|
||||
torrentFilename = launch.torrentFilename,
|
||||
torrentMagnetUri = launch.torrentMagnetUri,
|
||||
torrentTrackers = launch.torrentTrackers,
|
||||
initialPositionMs = launch.initialPositionMs,
|
||||
initialProgressFraction = launch.initialProgressFraction,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ object P2pSettingsRepository {
|
|||
if (p2pEnabled == enabled) return
|
||||
p2pEnabled = enabled
|
||||
P2pSettingsStorage.saveP2pEnabled(enabled)
|
||||
if (!enabled) {
|
||||
P2pStreamingEngine.shutdown()
|
||||
}
|
||||
publish()
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +97,7 @@ data class P2pStreamRequest(
|
|||
val infoHash: String,
|
||||
val fileIdx: Int?,
|
||||
val filename: String? = null,
|
||||
val magnetUri: String? = null,
|
||||
val trackers: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
|
|
@ -119,6 +123,8 @@ class P2pStreamingException(message: String) : Exception(message)
|
|||
|
||||
expect object P2pStreamingEngine {
|
||||
val state: StateFlow<P2pStreamingState>
|
||||
fun warmup()
|
||||
fun cooldownWarmup()
|
||||
suspend fun startStream(request: P2pStreamRequest): String
|
||||
fun stopStream()
|
||||
fun shutdown()
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ data class PlayerLaunch(
|
|||
val torrentInfoHash: String? = null,
|
||||
val torrentFileIdx: Int? = null,
|
||||
val torrentFilename: String? = null,
|
||||
val torrentMagnetUri: String? = null,
|
||||
val torrentTrackers: List<String> = emptyList(),
|
||||
val initialPositionMs: Long = 0L,
|
||||
val initialProgressFraction: Float? = null,
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ fun PlayerScreen(
|
|||
torrentInfoHash: String? = null,
|
||||
torrentFileIdx: Int? = null,
|
||||
torrentFilename: String? = null,
|
||||
torrentMagnetUri: String? = null,
|
||||
torrentTrackers: List<String> = emptyList(),
|
||||
initialPositionMs: Long = 0L,
|
||||
initialProgressFraction: Float? = null,
|
||||
|
|
@ -248,6 +249,7 @@ fun PlayerScreen(
|
|||
var activeTorrentInfoHash by rememberSaveable { mutableStateOf(torrentInfoHash) }
|
||||
var activeTorrentFileIdx by rememberSaveable { mutableStateOf(torrentFileIdx) }
|
||||
var activeTorrentFilename by rememberSaveable { mutableStateOf(torrentFilename) }
|
||||
var activeTorrentMagnetUri by rememberSaveable { mutableStateOf(torrentMagnetUri) }
|
||||
var activeTorrentTrackers by remember { mutableStateOf(torrentTrackers) }
|
||||
var p2pResolvedSourceUrl by remember { mutableStateOf<String?>(null) }
|
||||
val activePlaybackIdentity = activeTorrentInfoHash
|
||||
|
|
@ -1203,6 +1205,7 @@ fun PlayerScreen(
|
|||
activeTorrentInfoHash = null
|
||||
activeTorrentFileIdx = null
|
||||
activeTorrentFilename = null
|
||||
activeTorrentMagnetUri = null
|
||||
activeTorrentTrackers = emptyList()
|
||||
p2pResolvedSourceUrl = null
|
||||
}
|
||||
|
|
@ -1230,11 +1233,12 @@ fun PlayerScreen(
|
|||
addonId = stream.addonId,
|
||||
requestHeaders = emptyMap(),
|
||||
responseHeaders = emptyMap(),
|
||||
filename = stream.behaviorHints.filename,
|
||||
filename = stream.p2pFilename,
|
||||
videoSize = stream.behaviorHints.videoSize,
|
||||
infoHash = infoHash,
|
||||
fileIdx = stream.fileIdx,
|
||||
sources = stream.sources,
|
||||
fileIdx = stream.p2pFileIdx,
|
||||
magnetUri = stream.torrentMagnetUri,
|
||||
sources = stream.p2pSourceHints,
|
||||
bingeGroup = stream.behaviorHints.bingeGroup,
|
||||
)
|
||||
}
|
||||
|
|
@ -1255,13 +1259,14 @@ fun PlayerScreen(
|
|||
season = activeSeasonNumber,
|
||||
episode = activeEpisodeNumber,
|
||||
)
|
||||
activeSourceUrl = p2pSentinelUrl(infoHash, stream.fileIdx)
|
||||
activeSourceUrl = p2pSentinelUrl(infoHash, stream.p2pFileIdx)
|
||||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = emptyMap()
|
||||
activeSourceResponseHeaders = emptyMap()
|
||||
activeTorrentInfoHash = infoHash
|
||||
activeTorrentFileIdx = stream.fileIdx
|
||||
activeTorrentFilename = stream.behaviorHints.filename
|
||||
activeTorrentFileIdx = stream.p2pFileIdx
|
||||
activeTorrentFilename = stream.p2pFilename
|
||||
activeTorrentMagnetUri = stream.torrentMagnetUri
|
||||
activeTorrentTrackers = stream.p2pTrackers
|
||||
activeStreamTitle = stream.streamLabel
|
||||
activeStreamSubtitle = stream.streamSubtitle
|
||||
|
|
@ -1313,13 +1318,14 @@ fun PlayerScreen(
|
|||
season = episode.season,
|
||||
episode = episode.episode,
|
||||
)
|
||||
activeSourceUrl = p2pSentinelUrl(infoHash, stream.fileIdx)
|
||||
activeSourceUrl = p2pSentinelUrl(infoHash, stream.p2pFileIdx)
|
||||
activeSourceAudioUrl = null
|
||||
activeSourceHeaders = emptyMap()
|
||||
activeSourceResponseHeaders = emptyMap()
|
||||
activeTorrentInfoHash = infoHash
|
||||
activeTorrentFileIdx = stream.fileIdx
|
||||
activeTorrentFilename = stream.behaviorHints.filename
|
||||
activeTorrentFileIdx = stream.p2pFileIdx
|
||||
activeTorrentFilename = stream.p2pFilename
|
||||
activeTorrentMagnetUri = stream.torrentMagnetUri
|
||||
activeTorrentTrackers = stream.p2pTrackers
|
||||
activeStreamTitle = stream.streamLabel
|
||||
activeStreamSubtitle = stream.streamSubtitle
|
||||
|
|
@ -1938,6 +1944,7 @@ fun PlayerScreen(
|
|||
activeTorrentInfoHash,
|
||||
activeTorrentFileIdx,
|
||||
activeTorrentFilename,
|
||||
activeTorrentMagnetUri,
|
||||
activeTorrentTrackers,
|
||||
p2pSettingsUiState.p2pEnabled,
|
||||
) {
|
||||
|
|
@ -1954,6 +1961,7 @@ fun PlayerScreen(
|
|||
p2pResolvedSourceUrl = null
|
||||
val requestedFileIdx = activeTorrentFileIdx
|
||||
val requestedFilename = activeTorrentFilename
|
||||
val requestedMagnetUri = activeTorrentMagnetUri
|
||||
val requestedTrackers = activeTorrentTrackers
|
||||
errorMessage = null
|
||||
playerController = null
|
||||
|
|
@ -1967,6 +1975,7 @@ fun PlayerScreen(
|
|||
infoHash = infoHash,
|
||||
fileIdx = requestedFileIdx,
|
||||
filename = requestedFilename,
|
||||
magnetUri = requestedMagnetUri,
|
||||
trackers = requestedTrackers,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ data class CachedStreamLink(
|
|||
val videoSize: Long? = null,
|
||||
val infoHash: String? = null,
|
||||
val fileIdx: Int? = null,
|
||||
val magnetUri: String? = null,
|
||||
val sources: List<String> = emptyList(),
|
||||
val bingeGroup: String? = null,
|
||||
)
|
||||
|
|
@ -52,6 +53,7 @@ object StreamLinkCacheRepository {
|
|||
videoSize: Long? = null,
|
||||
infoHash: String? = null,
|
||||
fileIdx: Int? = null,
|
||||
magnetUri: String? = null,
|
||||
sources: List<String> = emptyList(),
|
||||
bingeGroup: String? = null,
|
||||
) {
|
||||
|
|
@ -67,6 +69,7 @@ object StreamLinkCacheRepository {
|
|||
videoSize = videoSize,
|
||||
infoHash = infoHash,
|
||||
fileIdx = fileIdx,
|
||||
magnetUri = magnetUri,
|
||||
sources = sources,
|
||||
bingeGroup = bingeGroup,
|
||||
)
|
||||
|
|
@ -92,7 +95,7 @@ object StreamLinkCacheRepository {
|
|||
StreamLinkCacheStorage.removeEntry(hashedKey(contentKey))
|
||||
return null
|
||||
}
|
||||
if (entry.url.isBlank() && entry.infoHash.isNullOrBlank()) {
|
||||
if (entry.url.isBlank() && entry.infoHash.isNullOrBlank() && entry.magnetUri.isNullOrBlank()) {
|
||||
StreamLinkCacheStorage.removeEntry(hashedKey(contentKey))
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ data class StreamItem(
|
|||
.firstOrNull { !it.isMagnetLink() }
|
||||
|
||||
val torrentMagnetUri: String?
|
||||
get() = listOfNotNull(url, externalUrl)
|
||||
get() = listOfNotNull(url, externalUrl, clientResolve?.magnetUri)
|
||||
.firstOrNull { it.isMagnetLink() }
|
||||
|
||||
val isDirectDebridStream: Boolean
|
||||
|
|
@ -61,10 +61,17 @@ data class StreamItem(
|
|||
val p2pInfoHash: String?
|
||||
get() = infoHash.normalizedInfoHash()
|
||||
?: clientResolve?.infoHash.normalizedInfoHash()
|
||||
?: clientResolve?.magnetUri.extractBtihInfoHash()
|
||||
?: torrentMagnetUri.extractBtihInfoHash()
|
||||
|
||||
val p2pFileIdx: Int?
|
||||
get() = fileIdx ?: clientResolve?.fileIdx
|
||||
|
||||
val p2pFilename: String?
|
||||
get() = behaviorHints.filename ?: clientResolve?.filename
|
||||
|
||||
val p2pTrackers: List<String>
|
||||
get() = sources
|
||||
get() = p2pSourceHints
|
||||
.asSequence()
|
||||
.filter { it.startsWith("tracker:") }
|
||||
.map { it.removePrefix("tracker:").trim() }
|
||||
|
|
@ -72,6 +79,10 @@ data class StreamItem(
|
|||
.distinct()
|
||||
.toList()
|
||||
|
||||
val p2pSourceHints: List<String>
|
||||
get() = (sources + clientResolve?.sources.orEmpty())
|
||||
.distinct()
|
||||
|
||||
val isAddonDebridCandidate: Boolean
|
||||
get() = isInstalledAddonStream && (needsLocalDebridResolve || isDirectDebridStream)
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -91,6 +92,8 @@ import coil3.compose.AsyncImage
|
|||
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
|
||||
import com.nuvio.app.features.debrid.DebridProviders
|
||||
import com.nuvio.app.features.debrid.DebridSettingsRepository
|
||||
import com.nuvio.app.features.p2p.P2pSettingsRepository
|
||||
import com.nuvio.app.features.p2p.P2pStreamingEngine
|
||||
import com.nuvio.app.features.player.PlayerSettingsRepository
|
||||
import com.nuvio.app.features.watchprogress.WatchProgressRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -140,6 +143,10 @@ fun StreamsScreen(
|
|||
DebridSettingsRepository.ensureLoaded()
|
||||
DebridSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val p2pSettings by remember {
|
||||
P2pSettingsRepository.ensureLoaded()
|
||||
P2pSettingsRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
val watchProgressUiState by remember {
|
||||
WatchProgressRepository.ensureLoaded()
|
||||
WatchProgressRepository.uiState
|
||||
|
|
@ -181,6 +188,15 @@ fun StreamsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
DisposableEffect(P2pSettingsRepository.isVisible, p2pSettings.p2pEnabled) {
|
||||
if (P2pSettingsRepository.isVisible && p2pSettings.p2pEnabled) {
|
||||
P2pStreamingEngine.warmup()
|
||||
}
|
||||
onDispose {
|
||||
P2pStreamingEngine.cooldownWarmup()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(type, videoId, seasonNumber, episodeNumber, manualSelection) {
|
||||
StreamsRepository.load(
|
||||
type = type,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
package com.nuvio.app.features.streams
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class StreamItemP2pMetadataTest {
|
||||
@Test
|
||||
fun usesClientResolveP2pMetadataWhenTopLevelFieldsAreMissing() {
|
||||
val magnet = "magnet:?xt=urn:btih:ABC123&dn=Movie&tr=udp%3A%2F%2Fmagnet.test%3A80%2Fannounce"
|
||||
val stream = StreamItem(
|
||||
name = "Resolved torrent",
|
||||
addonName = "Addon",
|
||||
addonId = "addon:test",
|
||||
sources = listOf(
|
||||
"tracker:udp://base.test:80/announce",
|
||||
"dht:ABC123",
|
||||
),
|
||||
clientResolve = StreamClientResolve(
|
||||
infoHash = "ABC123",
|
||||
fileIdx = 2,
|
||||
magnetUri = magnet,
|
||||
filename = "movie.mkv",
|
||||
sources = listOf(
|
||||
"tracker:udp://client.test:80/announce",
|
||||
"tracker:udp://base.test:80/announce",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals("ABC123", stream.p2pInfoHash)
|
||||
assertEquals(2, stream.p2pFileIdx)
|
||||
assertEquals("movie.mkv", stream.p2pFilename)
|
||||
assertEquals(magnet, stream.torrentMagnetUri)
|
||||
assertEquals(
|
||||
listOf(
|
||||
"udp://base.test:80/announce",
|
||||
"udp://client.test:80/announce",
|
||||
),
|
||||
stream.p2pTrackers,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun extractsP2pInfoHashFromClientResolveMagnet() {
|
||||
val stream = StreamItem(
|
||||
name = "Magnet-only torrent",
|
||||
addonName = "Addon",
|
||||
addonId = "addon:test",
|
||||
clientResolve = StreamClientResolve(
|
||||
magnetUri = "magnet:?xt=urn:btih:def456&dn=Movie",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals("def456", stream.p2pInfoHash)
|
||||
assertEquals("magnet:?xt=urn:btih:def456&dn=Movie", stream.torrentMagnetUri)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,14 @@ actual object P2pStreamingEngine {
|
|||
private val _state = MutableStateFlow<P2pStreamingState>(P2pStreamingState.Idle)
|
||||
actual val state: StateFlow<P2pStreamingState> = _state.asStateFlow()
|
||||
|
||||
actual fun warmup() {
|
||||
_state.value = P2pStreamingState.Idle
|
||||
}
|
||||
|
||||
actual fun cooldownWarmup() {
|
||||
_state.value = P2pStreamingState.Idle
|
||||
}
|
||||
|
||||
actual suspend fun startStream(request: P2pStreamRequest): String {
|
||||
val message = "P2P streaming is not available on this platform"
|
||||
_state.value = P2pStreamingState.Error(message)
|
||||
|
|
|
|||
96
scripts/build-torrserver-android.sh
Executable file
96
scripts/build-torrserver-android.sh
Executable file
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TORRSERVER_DIR="${TORRSERVER_DIR:-"${ROOT_DIR}/vendor/TorrServer/server"}"
|
||||
OUT_DIR="${OUT_DIR:-"${ROOT_DIR}/composeApp/src/androidMain/jniLibs"}"
|
||||
GO_BIN="${GO_BIN:-go}"
|
||||
UPX_BIN="${UPX_BIN:-upx}"
|
||||
SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-"${HOME}/Library/Android/sdk"}}"
|
||||
NDK_ROOT="${ANDROID_NDK_HOME:-}"
|
||||
export GOCACHE="${GOCACHE:-"${ROOT_DIR}/build/go-cache"}"
|
||||
export GOMODCACHE="${GOMODCACHE:-"${ROOT_DIR}/build/go-mod-cache"}"
|
||||
|
||||
if ! command -v "${GO_BIN}" >/dev/null 2>&1; then
|
||||
echo "Go toolchain not found. Install Go or set GO_BIN=/path/to/go." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "${TORRSERVER_DIR}" ]]; then
|
||||
echo "TorrServer source not found at ${TORRSERVER_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${NDK_ROOT}" ]]; then
|
||||
if [[ ! -d "${SDK_ROOT}/ndk" ]]; then
|
||||
echo "Android NDK not found. Set ANDROID_HOME, ANDROID_SDK_ROOT, or ANDROID_NDK_HOME." >&2
|
||||
exit 1
|
||||
fi
|
||||
NDK_VERSION="$(ls -1 "${SDK_ROOT}/ndk" | sort | tail -n 1)"
|
||||
NDK_ROOT="${SDK_ROOT}/ndk/${NDK_VERSION}"
|
||||
fi
|
||||
|
||||
PREBUILT_ROOT="${NDK_ROOT}/toolchains/llvm/prebuilt"
|
||||
HOST_TAG=""
|
||||
for candidate in darwin-x86_64 linux-x86_64; do
|
||||
if [[ -d "${PREBUILT_ROOT}/${candidate}" ]]; then
|
||||
HOST_TAG="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "${HOST_TAG}" ]]; then
|
||||
echo "Could not find an LLVM prebuilt toolchain under ${PREBUILT_ROOT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOOLCHAIN="${PREBUILT_ROOT}/${HOST_TAG}"
|
||||
LDFLAGS="${LDFLAGS:-"-s -w -checklinkname=0"}"
|
||||
BUILD_FLAGS=(-tags=nosqlite -trimpath -ldflags="${LDFLAGS}")
|
||||
mkdir -p "${GOCACHE}" "${GOMODCACHE}"
|
||||
|
||||
build_abi() {
|
||||
local abi="$1"
|
||||
local goarch="$2"
|
||||
local goarm="$3"
|
||||
local triple="$4"
|
||||
local api_level="$5"
|
||||
local cc="${TOOLCHAIN}/bin/${triple}${api_level}-clang"
|
||||
local cxx="${TOOLCHAIN}/bin/${triple}${api_level}-clang++"
|
||||
local output="${OUT_DIR}/${abi}/libtorrserver.so"
|
||||
|
||||
if [[ ! -x "${cc}" ]]; then
|
||||
echo "Compiler not found: ${cc}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "${output}")"
|
||||
echo "Building ${abi} -> ${output}"
|
||||
|
||||
local env_vars=(
|
||||
GOOS=android
|
||||
GOARCH="${goarch}"
|
||||
CGO_ENABLED=1
|
||||
CC="${cc}"
|
||||
CXX="${cxx}"
|
||||
)
|
||||
if [[ -n "${goarm}" ]]; then
|
||||
env_vars+=(GOARM="${goarm}")
|
||||
fi
|
||||
|
||||
(
|
||||
cd "${TORRSERVER_DIR}"
|
||||
env "${env_vars[@]}" "${GO_BIN}" build "${BUILD_FLAGS[@]}" -o "${output}" ./cmd
|
||||
)
|
||||
chmod 755 "${output}"
|
||||
if [[ "${USE_UPX:-0}" == "1" ]] && command -v "${UPX_BIN}" >/dev/null 2>&1; then
|
||||
"${UPX_BIN}" -q "${output}" || echo "UPX compression failed for ${output}; leaving uncompressed" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
build_abi "arm64-v8a" "arm64" "" "aarch64-linux-android" "21"
|
||||
build_abi "armeabi-v7a" "arm" "7" "armv7a-linux-androideabi" "21"
|
||||
build_abi "x86" "386" "" "i686-linux-android" "21"
|
||||
build_abi "x86_64" "amd64" "" "x86_64-linux-android" "21"
|
||||
|
||||
echo "TorrServer Android binaries updated in ${OUT_DIR}"
|
||||
|
|
@ -140,46 +140,75 @@ android_flavor_task_part() {
|
|||
esac
|
||||
}
|
||||
|
||||
android_apk_path() {
|
||||
android_apk_dir() {
|
||||
local flavor="$1"
|
||||
|
||||
case "$flavor" in
|
||||
full)
|
||||
echo "$ROOT_DIR/composeApp/build/outputs/apk/full/debug/composeApp-full-debug.apk"
|
||||
echo "$ROOT_DIR/composeApp/build/outputs/apk/full/debug"
|
||||
;;
|
||||
playstore)
|
||||
echo "$ROOT_DIR/composeApp/build/outputs/apk/playstore/debug/composeApp-playstore-debug.apk"
|
||||
echo "$ROOT_DIR/composeApp/build/outputs/apk/playstore/debug"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
build_android_apk() {
|
||||
android_device_primary_abi() {
|
||||
local serial="$1"
|
||||
adb -s "$serial" shell getprop ro.product.cpu.abi | tr -d '\r'
|
||||
}
|
||||
|
||||
android_split_apk_path() {
|
||||
local flavor="$1"
|
||||
local flavor_task_part
|
||||
local serial="$2"
|
||||
local apk_dir
|
||||
local abi
|
||||
local apk_path
|
||||
|
||||
flavor_task_part="$(android_flavor_task_part "$flavor")"
|
||||
apk_path="$(android_apk_path "$flavor")"
|
||||
apk_dir="$(android_apk_dir "$flavor")"
|
||||
abi="$(android_device_primary_abi "$serial")"
|
||||
|
||||
echo "Building Android $flavor debug APK..." >&2
|
||||
"$GRADLEW" ":composeApp:assemble${flavor_task_part}Debug" >&2
|
||||
apk_path="$(find "$apk_dir" -maxdepth 1 -type f -name "*-${abi}-debug.apk" | sort | head -n 1)"
|
||||
if [[ -z "$apk_path" ]]; then
|
||||
apk_path="$(find "$apk_dir" -maxdepth 1 -type f -name "*${abi}*.apk" | sort | head -n 1)"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$apk_path" ]]; then
|
||||
echo "Expected APK not found at: $apk_path" >&2
|
||||
if [[ -z "$apk_path" || ! -f "$apk_path" ]]; then
|
||||
echo "Expected split APK for ABI '$abi' not found in: $apk_dir" >&2
|
||||
find "$apk_dir" -maxdepth 1 -type f -name "*.apk" -print >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$apk_path"
|
||||
}
|
||||
|
||||
build_android_apks() {
|
||||
local flavor="$1"
|
||||
local flavor_task_part
|
||||
local apk_dir
|
||||
|
||||
flavor_task_part="$(android_flavor_task_part "$flavor")"
|
||||
apk_dir="$(android_apk_dir "$flavor")"
|
||||
|
||||
echo "Building Android $flavor debug split APKs..." >&2
|
||||
"$GRADLEW" ":composeApp:assemble${flavor_task_part}Debug" >&2
|
||||
|
||||
if ! find "$apk_dir" -maxdepth 1 -type f -name "*.apk" | grep -q .; then
|
||||
echo "Expected split APKs not found in: $apk_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_and_launch_android() {
|
||||
local device_label="$1"
|
||||
local apk_path="$2"
|
||||
local flavor="$2"
|
||||
shift 2
|
||||
|
||||
local apk_path
|
||||
local serial
|
||||
for serial in "$@"; do
|
||||
echo "Installing on $device_label $serial..."
|
||||
apk_path="$(android_split_apk_path "$flavor" "$serial")"
|
||||
echo "Installing on $device_label $serial: $apk_path"
|
||||
adb -s "$serial" install -r "$apk_path"
|
||||
|
||||
echo "Launching app on $serial..."
|
||||
|
|
@ -248,10 +277,9 @@ run_android_emulator() {
|
|||
wait_for_android_emulator "$serial"
|
||||
done
|
||||
|
||||
local apk_path
|
||||
apk_path="$(build_android_apk "$flavor")"
|
||||
build_android_apks "$flavor"
|
||||
|
||||
install_and_launch_android "emulator" "$apk_path" "${booted_serials[@]}"
|
||||
install_and_launch_android "emulator" "$flavor" "${booted_serials[@]}"
|
||||
}
|
||||
|
||||
run_android_physical() {
|
||||
|
|
@ -280,10 +308,9 @@ run_android_physical() {
|
|||
wait_for_android_device "$serial"
|
||||
done
|
||||
|
||||
local apk_path
|
||||
apk_path="$(build_android_apk "$flavor")"
|
||||
build_android_apks "$flavor"
|
||||
|
||||
install_and_launch_android "physical device" "$apk_path" "${serials[@]}"
|
||||
install_and_launch_android "physical device" "$flavor" "${serials[@]}"
|
||||
}
|
||||
|
||||
run_ios_simulator() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue