Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ plugins {

android {
namespace = "com.arflix.tv"
compileSdk = 35
compileSdk = 36

flavorDimensions += "distribution"

Expand Down Expand Up @@ -56,10 +56,12 @@ android {
create("play") {
dimension = "distribution"
buildConfigField("Boolean", "SELF_UPDATE_ENABLED", "false")
buildConfigField("Boolean", "FEATURE_PLUGINS_ENABLED", "false")
}
create("sideload") {
dimension = "distribution"
buildConfigField("Boolean", "SELF_UPDATE_ENABLED", "true")
buildConfigField("Boolean", "FEATURE_PLUGINS_ENABLED", "true")
}
}

Expand Down Expand Up @@ -140,9 +142,7 @@ android {
isCoreLibraryDesugaringEnabled = true
}

kotlinOptions {
jvmTarget = "17"
}


buildFeatures {
compose = true
Expand All @@ -154,8 +154,7 @@ android {
excludes += setOf(
"/META-INF/{AL2.0,LGPL2.1}",
"/META-INF/LICENSE*",
"/META-INF/NOTICE*",
)
"/META-INF/NOTICE*", "META-INF/versions/9/OSGI-INF/MANIFEST.MF")
}
jniLibs {
useLegacyPackaging = false // Required for 16KB page size support
Expand Down Expand Up @@ -240,8 +239,8 @@ dependencies {
// with "Unable to read Kotlin metadata due to unsupported metadata
// version" because Hilt parses generated `@Module` classes that carry
// Kotlin 2.1's newer metadata format.
implementation("com.google.dagger:hilt-android:2.54")
ksp("com.google.dagger:hilt-compiler:2.54")
implementation("com.google.dagger:hilt-android:2.57")
ksp("com.google.dagger:hilt-compiler:2.57")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")

// Leanback (TV compliance, browse fragments if needed)
Expand Down Expand Up @@ -536,3 +535,32 @@ detekt {
// Don't fail build on issues (use baseline instead)
ignoreFailures = true
}


kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
freeCompilerArgs.add("-Xskip-metadata-version-check")
}
}

dependencies {
ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0")
annotationProcessor("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0")

// Plugin system dependencies (Sideload flavor only)
add("sideloadImplementation", files("libs/quickjs-kt-android-1.0.5-nuvio.aar"))
add("sideloadImplementation", "com.fasterxml.jackson.core:jackson-databind:2.17.0")
add("sideloadImplementation", "com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0")
add("sideloadImplementation", "com.github.Blatzar:NiceHttp:0.4.11")
add("sideloadImplementation", "org.conscrypt:conscrypt-android:2.5.3")
add("sideloadImplementation", "com.github.recloudstream.cloudstream:library:v4.7.0") {
exclude(group = "org.mozilla", module = "rhino")
}
add("sideloadImplementation", "org.webjars.npm:crypto-js:4.2.0")

// Moshi - used in both sideload plugins and main data store
implementation("com.squareup.moshi:moshi:1.15.1")
implementation("com.squareup.moshi:moshi-kotlin:1.15.1")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.1")
}
Binary file added app/libs/quickjs-kt-android-1.0.5-nuvio.aar
Binary file not shown.
2 changes: 0 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,3 @@
</application>

</manifest>


4 changes: 0 additions & 4 deletions app/src/main/kotlin/com/arflix/tv/ArflixApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,3 @@ class ArflixApplication : Application(), Configuration.Provider, ImageLoaderFact
}
}
}




51 changes: 51 additions & 0 deletions app/src/main/kotlin/com/arflix/tv/core/plugin/PluginSafety.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.arflix.tv.core.plugin

internal object PluginSafety {

// List of known dangerous package names/prefixes that should be blocked
private val BLOCKED_PACKAGES = setOf(
"com.google",
"android",
"java",
"javax",
"kotlin",
"com.arflix.tv.core" // Prevent plugins from shadowing our own core logic
)

// Allowed plugin file extensions
private val ALLOWED_EXTENSIONS = setOf("cs3", "apk", "dex", "js")

/**
* Validates a plugin based on its metadata before allowing it to load.
*/
fun isSafeToLoad(
pluginName: String?,
pluginPackage: String?,
filename: String?
): Boolean {
// Basic presence checks
if (pluginName.isNullOrBlank() || filename.isNullOrBlank()) {
return false
}

// Validate extension
val ext = filename.substringAfterLast('.', "").lowercase()
if (ext !in ALLOWED_EXTENSIONS) {
return false
}

// Validate package name (if provided) to prevent namespace shadowing
if (pluginPackage != null) {
if (BLOCKED_PACKAGES.any { pluginPackage.startsWith(it, ignoreCase = true) }) {
return false
}
}

// No path traversal allowed
if (filename.contains("/") || filename.contains("\\") || filename.contains("..")) {
return false
}

return true
}
}
18 changes: 18 additions & 0 deletions app/src/main/kotlin/com/arflix/tv/core/plugin/TestDiagnostics.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.arflix.tv.core.plugin

import android.util.Log

private const val TAG = "TestDiagnostics"

/**
* Collects diagnostic steps during a scraper test run.
* Each step is a status line like "DEX loaded: 2 MainAPIs" or "Search: 5 results for 'The Matrix'".
*/
data class TestDiagnostics(
val steps: MutableList<String> = mutableListOf()
) {
fun addStep(step: String) {
steps.add(step)
Log.d(TAG, step)
}
}
22 changes: 10 additions & 12 deletions app/src/main/kotlin/com/arflix/tv/data/api/SupabaseClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ interface SupabaseApi {
@Query("limit") limit: Int = 50,
@Query("offset") offset: Int? = null
): List<WatchHistoryRecord>

@POST("rest/v1/watch_history")
suspend fun upsertWatchHistory(
@Header("Authorization") auth: String,
@Header("apikey") apiKey: String = Constants.SUPABASE_ANON_KEY,
@Header("Prefer") prefer: String = "resolution=merge-duplicates",
@Body item: WatchHistoryRecord
)

@GET("rest/v1/watch_history")
suspend fun getWatchHistoryItem(
@Header("Authorization") auth: String,
Expand Down Expand Up @@ -73,7 +73,7 @@ interface SupabaseApi {
@Query("episode") episode: String? = null,
@Query("source") source: String? = null
)

@retrofit2.http.HTTP(method = "DELETE", path = "rest/v1/watch_history", hasBody = false)
suspend fun deleteWatchHistoryByIds(
@Header("Authorization") auth: String,
Expand All @@ -82,23 +82,23 @@ interface SupabaseApi {
)

// ========== User Profiles ==========

@GET("rest/v1/profiles")
suspend fun getProfile(
@Header("Authorization") auth: String,
@Header("apikey") apiKey: String = Constants.SUPABASE_ANON_KEY,
@Query("id") userId: String,
@Query("select") select: String = "*"
): List<UserProfile>

@PATCH("rest/v1/profiles")
suspend fun updateProfile(
@Header("Authorization") auth: String,
@Header("apikey") apiKey: String = Constants.SUPABASE_ANON_KEY,
@Query("id") userId: String,
@Body profile: UserProfileUpdate
)

// ========== Watchlist ==========

@GET("rest/v1/watchlist")
Expand Down Expand Up @@ -130,7 +130,7 @@ interface SupabaseApi {
)

// ========== Watched Status (from Trakt sync) ==========

@GET("rest/v1/watched_movies")
suspend fun getWatchedMovies(
@Header("Authorization") auth: String,
Expand All @@ -142,7 +142,7 @@ interface SupabaseApi {
@Query("offset") offset: Int = 0,
@Query("limit") limit: Int = 1000
): List<WatchedMovieRecord>

@GET("rest/v1/watched_episodes")
suspend fun getWatchedEpisodes(
@Header("Authorization") auth: String,
Expand All @@ -165,15 +165,15 @@ interface SupabaseApi {
@Query("tmdb_id") tmdbId: String,
@Query("select") select: String = "user_id,profile_id,tmdb_id,show_trakt_id,season,episode,trakt_episode_id,tmdb_episode_id,watched_at,updated_at,source"
): List<WatchedEpisodeRecord>

@POST("rest/v1/watched_movies")
suspend fun markMovieWatched(
@Header("Authorization") auth: String,
@Header("apikey") apiKey: String = Constants.SUPABASE_ANON_KEY,
@Header("Prefer") prefer: String = "resolution=merge-duplicates",
@Body record: WatchedMovieRecord
)

@POST("rest/v1/watched_episodes")
suspend fun markEpisodeWatched(
@Header("Authorization") auth: String,
Expand Down Expand Up @@ -406,5 +406,3 @@ data class SyncStateRecord(
@SerializedName("last_error") val lastError: String? = null,
@SerializedName("updated_at") val updatedAt: String? = null
)


20 changes: 10 additions & 10 deletions app/src/main/kotlin/com/arflix/tv/data/api/TmdbApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import retrofit2.http.Query
* TMDB API interface
*/
interface TmdbApi {

@GET("trending/movie/day")
suspend fun getTrendingMovies(
@Query("api_key") apiKey: String,
Expand All @@ -23,7 +23,7 @@ interface TmdbApi {
@Query("language") language: String? = null,
@Query("page") page: Int = 1
): TmdbListResponse

@GET("discover/tv")
suspend fun discoverTv(
@Query("api_key") apiKey: String,
Expand Down Expand Up @@ -59,21 +59,21 @@ interface TmdbApi {
@Query("language") language: String? = null,
@Query("page") page: Int = 1
): TmdbListResponse

@GET("movie/{movie_id}")
suspend fun getMovieDetails(
@Path("movie_id") movieId: Int,
@Query("api_key") apiKey: String,
@Query("language") language: String? = null
): TmdbMovieDetails

@GET("tv/{tv_id}")
suspend fun getTvDetails(
@Path("tv_id") tvId: Int,
@Query("api_key") apiKey: String,
@Query("language") language: String? = null
): TmdbTvDetails

@GET("tv/{tv_id}/season/{season_number}")
suspend fun getTvSeason(
@Path("tv_id") tvId: Int,
Expand All @@ -89,15 +89,15 @@ interface TmdbApi {
@Path("episode_number") episodeNumber: Int,
@Query("api_key") apiKey: String
): TmdbExternalIds

@GET("{media_type}/{id}/credits")
suspend fun getCredits(
@Path("media_type") mediaType: String,
@Path("id") id: Int,
@Query("api_key") apiKey: String,
@Query("language") language: String? = null
): TmdbCreditsResponse

@GET("{media_type}/{id}/similar")
suspend fun getSimilar(
@Path("media_type") mediaType: String,
Expand All @@ -120,15 +120,15 @@ interface TmdbApi {
@Path("id") id: Int,
@Query("api_key") apiKey: String
): TmdbImagesResponse

@GET("{media_type}/{id}/videos")
suspend fun getVideos(
@Path("media_type") mediaType: String,
@Path("id") id: Int,
@Query("api_key") apiKey: String,
@Query("language") language: String? = null
): TmdbVideosResponse

@GET("person/{person_id}")
suspend fun getPersonDetails(
@Path("person_id") personId: Int,
Expand Down Expand Up @@ -160,7 +160,7 @@ interface TmdbApi {
@Path("tv_id") tvId: Int,
@Query("api_key") apiKey: String
): TmdbWatchProvidersResponse

@GET("search/multi")
suspend fun searchMulti(
@Query("api_key") apiKey: String,
Expand Down
Loading