Przeglądaj źródła

scrapper improvements

scrapper improvements

scrapper improvements

scrapper improvements
Milan Jurkulak 2 lat temu
rodzic
commit
132c10161c
70 zmienionych plików z 1104 dodań i 734 usunięć
  1. 2 1
      app/build.gradle.kts
  2. 22 8
      app/objectbox-models/default.json
  3. 18 81
      app/objectbox-models/default.json.bak
  4. 1 1
      app/proguard-rules.pro
  5. 2 2
      app/src/main/AndroidManifest.xml
  6. 1 0
      app/src/main/assets/adblock.txt
  7. 10 0
      app/src/main/kotlin/org/mjdev/tvapp/data/local/Movie.kt
  8. 1 25
      app/src/main/kotlin/org/mjdev/tvapp/database/DAO.kt
  9. 32 11
      app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt
  10. 16 120
      app/src/main/kotlin/org/mjdev/tvapp/sync/SyncAdapter.kt
  11. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/sync/auth/Authenticator.kt
  12. 9 1
      app/src/main/kotlin/org/mjdev/tvapp/sync/auth/AuthenticatorService.kt
  13. 46 0
      app/src/main/kotlin/org/mjdev/tvapp/sync/base/SyncItems.kt
  14. 24 0
      app/src/main/kotlin/org/mjdev/tvapp/sync/base/Syncer.kt
  15. 9 1
      app/src/main/kotlin/org/mjdev/tvapp/sync/provider/DataProvider.kt
  16. 72 0
      app/src/main/kotlin/org/mjdev/tvapp/sync/syncers/GithubMoviesSyncer.kt
  17. 42 0
      app/src/main/kotlin/org/mjdev/tvapp/sync/syncers/WebMovieSyncer.kt
  18. 0 2
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt
  19. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt
  20. 0 24
      buildSrc/build.gradle.kts
  21. 35 5
      buildSrc/src/main/kotlin/org/mjdev/gradle/dependency/DependencyHandler.kt
  22. 37 35
      buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/SigningConfigs.kt
  23. 11 8
      buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/TestedExtension.kt
  24. 1 1
      config/version.properties
  25. 3 1
      release-notes.txt
  26. 5 3
      tvlib/build.gradle.kts
  27. 32 32
      tvlib/src/main/kotlin/org/mjdev/tvlib/database/convert/CustomPropertyConverter.kt
  28. 30 30
      tvlib/src/main/kotlin/org/mjdev/tvlib/database/convert/EncryptionConverter.kt
  29. 23 23
      tvlib/src/main/kotlin/org/mjdev/tvlib/database/convert/PropertyEnumConverter.kt
  30. 21 21
      tvlib/src/main/kotlin/org/mjdev/tvlib/database/convert/PropertyJsonConverter.kt
  31. 33 33
      tvlib/src/main/kotlin/org/mjdev/tvlib/database/convert/StringListConverter.kt
  32. 53 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/database/extensions/ObjectBoxExt.kt
  33. 14 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/exception/CodeException.kt
  34. 55 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ApiResponseExt.kt
  35. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ColorExt.kt
  36. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ContextExt.kt
  37. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/CursorExt.kt
  38. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/DrawableExt.kt
  39. 6 54
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt
  40. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/HiltExt.kt
  41. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/LifecycleExt.kt
  42. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/MediaItemExt.kt
  43. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavBackStackEntryExt.kt
  44. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavExt.kt
  45. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavGraphBuilderExt.kt
  46. 19 18
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/OkHttpClientExt.kt
  47. 10 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/StringExt.kt
  48. 1 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ViewExt.kt
  49. 10 43
      tvlib/src/main/kotlin/org/mjdev/tvlib/glide/OkHttpUrlLoaderEx.kt
  50. 17 12
      tvlib/src/main/kotlin/org/mjdev/tvlib/glide/sources/albumart/AlbumArtLoaderFactory.kt
  51. 78 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/AccessTokenInterceptor.kt
  52. 11 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/AuthenticationError.kt
  53. 4 18
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/CacheInterceptor.kt
  54. 53 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/ErrorInterceptor.kt
  55. 11 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/GenericMessageException.kt
  56. 21 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/ISessionRepository.kt
  57. 43 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/NetworkConnectionInterceptor.kt
  58. 11 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/ServerException.kt
  59. 14 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/SessionPreferenceManager.kt
  60. 5 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/UserAgentInterceptor.kt
  61. 13 14
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/DailyMotionVideoView.kt
  62. 0 28
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/SettingsPage.kt
  63. 29 12
      tvlib/src/main/kotlin/org/mjdev/tvlib/updater/AppUpdater.kt
  64. 16 29
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/WebVideoScrapper.kt
  65. 8 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/AdBlock.kt
  66. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/AdBlockInterceptor.kt
  67. 30 24
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/fetcher/BrowserFetcher.kt
  68. 4 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/DocElement.kt
  69. 5 5
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/DomTreeElement.kt
  70. 12 0
      wiki/Release-Notes-1.2.2.md

+ 2 - 1
app/build.gradle.kts

@@ -219,10 +219,11 @@ dependencies {
     retrofitDependencies()
     sandwichDependencies()
     glideDependencies()
+//    imageIODependencies()
     exoPlayerDependencies()
-    testDependencies()
     encryptDependencies()
     permissionsDependencies()
     anotherDependencies()
     widgetDependencies()
+    testDependencies()
 }

+ 22 - 8
app/objectbox-models/default.json

@@ -45,22 +45,30 @@
         {
           "id": "2:4325777566417003871",
           "name": "category",
-          "type": 9
+          "indexId": "7:4917936932975926084",
+          "type": 9,
+          "flags": 2048
         },
         {
           "id": "3:3140383628530752513",
           "name": "studio",
-          "type": 9
+          "indexId": "1:7948152039652986882",
+          "type": 9,
+          "flags": 2048
         },
         {
           "id": "4:7177523158231354514",
           "name": "title",
-          "type": 9
+          "indexId": "3:4293154197130924572",
+          "type": 9,
+          "flags": 2048
         },
         {
           "id": "5:6928102760022948176",
           "name": "description",
-          "type": 9
+          "indexId": "4:647869640726053234",
+          "type": 9,
+          "flags": 2048
         },
         {
           "id": "6:3074162147955651553",
@@ -75,12 +83,16 @@
         {
           "id": "8:6917239407098203120",
           "name": "uri",
-          "type": 9
+          "indexId": "5:2693739985883588422",
+          "type": 9,
+          "flags": 2080
         },
         {
           "id": "10:3350489478053652777",
           "name": "country",
-          "type": 9
+          "indexId": "2:2822558398014900887",
+          "type": 9,
+          "flags": 2048
         },
         {
           "id": "11:3647500054699987607",
@@ -96,7 +108,9 @@
         {
           "id": "13:503445141634537419",
           "name": "subtitle",
-          "type": 9
+          "indexId": "6:8857753723584819000",
+          "type": 9,
+          "flags": 2048
         }
       ],
       "relations": []
@@ -280,7 +294,7 @@
     }
   ],
   "lastEntityId": "6:8612359832519392835",
-  "lastIndexId": "0:0",
+  "lastIndexId": "7:4917936932975926084",
   "lastRelationId": "0:0",
   "lastSequenceId": "0:0",
   "modelVersion": 5,

+ 18 - 81
app/objectbox-models/default.json.bak

@@ -215,85 +215,6 @@
       ],
       "relations": []
     },
-    {
-      "id": "4:1987880888030669889",
-      "lastPropertyId": "14:6587709986428549544",
-      "name": "User",
-      "properties": [
-        {
-          "id": "1:8489281024233193571",
-          "name": "id",
-          "type": 6,
-          "flags": 1
-        },
-        {
-          "id": "2:3068422320211502547",
-          "name": "sub",
-          "type": 9
-        },
-        {
-          "id": "3:7880080743593438568",
-          "name": "givenName",
-          "type": 9
-        },
-        {
-          "id": "4:7091128848714558596",
-          "name": "familyName",
-          "type": 9
-        },
-        {
-          "id": "5:3275722102836586086",
-          "name": "nickname",
-          "type": 9
-        },
-        {
-          "id": "6:539261702815051564",
-          "name": "fullName",
-          "type": 9
-        },
-        {
-          "id": "7:5697902777673765209",
-          "name": "pictureUrl",
-          "type": 9
-        },
-        {
-          "id": "8:1398381634980613029",
-          "name": "locale",
-          "type": 9
-        },
-        {
-          "id": "9:7614335227423573086",
-          "name": "lastUpdated",
-          "type": 9
-        },
-        {
-          "id": "10:3865959571559170686",
-          "name": "pinCode",
-          "type": 9
-        },
-        {
-          "id": "11:5012672523655215079",
-          "name": "settingsJson",
-          "type": 9
-        },
-        {
-          "id": "12:7864280554037368021",
-          "name": "age",
-          "type": 5
-        },
-        {
-          "id": "13:3813998053361806387",
-          "name": "email",
-          "type": 9
-        },
-        {
-          "id": "14:6587709986428549544",
-          "name": "emailVerified",
-          "type": 1
-        }
-      ],
-      "relations": []
-    },
     {
       "id": "5:6626640907745307806",
       "lastPropertyId": "4:4895179631110667602",
@@ -364,11 +285,27 @@
   "lastSequenceId": "0:0",
   "modelVersion": 5,
   "modelVersionParserMinimum": 5,
-  "retiredEntityUids": [],
+  "retiredEntityUids": [
+    1987880888030669889
+  ],
   "retiredIndexUids": [],
   "retiredPropertyUids": [
     2336450843545835846,
-    1211648868008137353
+    1211648868008137353,
+    8489281024233193571,
+    3068422320211502547,
+    7880080743593438568,
+    7091128848714558596,
+    3275722102836586086,
+    539261702815051564,
+    5697902777673765209,
+    1398381634980613029,
+    7614335227423573086,
+    3865959571559170686,
+    5012672523655215079,
+    7864280554037368021,
+    3813998053361806387,
+    6587709986428549544
   ],
   "retiredRelationUids": [],
   "version": 1

+ 1 - 1
app/proguard-rules.pro

@@ -25,7 +25,7 @@
 
 -keep,allowobfuscation,allowshrinking interface retrofit2.Call
 -keep,allowobfuscation,allowshrinking class retrofit2.Response
--keep,allowobfuscation,allowshrinking class kotlin.coroutines.
+-keep,allowobfuscation,allowshrinking class kotlin.coroutines.*
 
 -dontwarn java.awt.font.FontRenderContext
 -dontwarn java.awt.font.LineBreakMeasurer

+ 2 - 2
app/src/main/AndroidManifest.xml

@@ -196,7 +196,7 @@
         </service>
 
         <service
-            android:name=".sync.AuthenticatorService"
+            android:name=".sync.auth.AuthenticatorService"
             android:exported="false">
 
             <intent-filter>
@@ -210,7 +210,7 @@
         </service>
 
         <provider
-            android:name=".sync.DataProvider"
+            android:name=".sync.provider.DataProvider"
             android:authorities="@string/sync_auth"
             android:enabled="true"
             android:exported="true"

+ 1 - 0
app/src/main/assets/adblock.txt

@@ -10,6 +10,7 @@
 *a.realsrv.com
 *stats.hprofits.com
 *tsyndicate.com
+*pagead2.googlesyndication.com
 *fonts.gstatic.com
 *fonts.googleapis.com
 *stats.g.doubleclick.net

+ 10 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/local/Movie.kt

@@ -13,6 +13,8 @@ import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 import io.objectbox.annotation.Entity
 import io.objectbox.annotation.Id
+import io.objectbox.annotation.Index
+import io.objectbox.annotation.Unique
 import org.mjdev.tvlib.interfaces.ItemWithBackground
 import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithId
@@ -42,27 +44,35 @@ class Movie :
     override var id: Long = 0
 
     @Json(name = "studio")
+    @Index
     var studio: String? = null
 
     @Json(name = "country")
+    @Index
     var country: String? = null
 
     @Json(name = "isNsfw")
     var isNsfw: Boolean = false
 
     @Json(name = "title")
+    @Index
     override var title: String? = null
 
     @Json(name = "description")
+    @Index
     override var description: String? = null
 
     @Json(name = "videoUri")
+    @Unique
+    @Index
     override var uri: String? = null
 
+    @Index
     override var subtitle: String? = null
         get() = field ?: studio
 
     @Json(name = "category")
+    @Index
     var category: String? = ""
         get() = (field ?: country)?.replaceFirstChar { ch ->
             ch.titlecase(Locale.ROOT)

+ 1 - 25
app/src/main/kotlin/org/mjdev/tvapp/database/DAO.kt

@@ -12,7 +12,6 @@ import android.content.Context
 import dagger.hilt.android.qualifiers.ApplicationContext
 import io.objectbox.Box
 import io.objectbox.BoxStore
-import io.objectbox.kotlin.awaitCallInTx
 import io.objectbox.kotlin.boxFor
 import org.mjdev.tvapp.BuildConfig
 import org.mjdev.tvapp.data.local.Category
@@ -29,30 +28,7 @@ class DAO(
 ) {
 
     companion object {
-
-        val DATABASE_NAME = BuildConfig.APPLICATION_ID
-            .replace(".", "_")
-
-        suspend inline fun <reified V, T> Box<T>.tx(
-            crossinline transaction: Box<T>.() -> V
-        ) = store.awaitCallInTx {
-            transaction(this)
-        }
-
-        // todo remove reflection
-        inline fun <reified T> Box<T>.findById(id: Long?): T? = if (id == null) all.firstOrNull()
-        else all.firstOrNull { o -> o.property("id") == id }
-
-        // todo remove reflection
-        inline fun <reified T> T.property(name: String): Any? = T::class.members.firstOrNull {
-            it.name == name
-        }?.call(this)
-
-        inline fun <T> with(receiver: T, block: T.() -> Unit): T {
-            block.invoke(receiver)
-            return receiver
-        }
-
+        val DATABASE_NAME = BuildConfig.APPLICATION_ID.replace(".", "_")
     }
 
     private val store: BoxStore by lazy {

+ 32 - 11
app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt

@@ -26,9 +26,11 @@ import org.mjdev.tvapp.repository.ApiService
 import org.mjdev.tvlib.helpers.cursor.AudioCursor
 import org.mjdev.tvlib.helpers.cursor.PhotoCursor
 import org.mjdev.tvlib.helpers.cursor.VideoCursor
-import org.mjdev.tvlib.network.CacheInterceptor
 import org.mjdev.tvlib.network.NetworkConnectivityService
 import org.mjdev.tvapp.database.DAO
+import org.mjdev.tvlib.helpers.http.NetworkConnectionInterceptor
+import org.mjdev.tvlib.helpers.http.UserAgentInterceptor
+import org.mjdev.tvlib.webscrapper.adblock.AdBlockInterceptor
 import retrofit2.Retrofit
 import retrofit2.converter.moshi.MoshiConverterFactory
 import javax.inject.Singleton
@@ -77,27 +79,46 @@ class ProvideModule {
 
     @Singleton
     @Provides
-    fun providesHttpLoggingInterceptor() = HttpLoggingInterceptor().apply {
-        level = HttpLoggingInterceptor.Level.BODY
-    }
+    fun providesHttpLoggingInterceptor() = HttpLoggingInterceptor().setLevel(
+        if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
+        else HttpLoggingInterceptor.Level.NONE
+    )
+
+//    @Singleton
+//    @Provides
+//    fun providesCacheInterceptor() = CacheInterceptor()
 
     @Singleton
     @Provides
-    fun providesCacheInterceptor() = CacheInterceptor()
+    fun provideAdBlockInterceptor(
+        @ApplicationContext
+        context: Context
+    ) = AdBlockInterceptor(context)
 
-//    @Singleton
-//    @Provides
-//    fun provideAdBlockInterceptor() = AdBlockInterceptor()
+    @Singleton
+    @Provides
+    fun provideUserAgentInterceptor() = UserAgentInterceptor()
+
+    @Singleton
+    @Provides
+    fun provideNetworkConnectionInterceptor(
+        @ApplicationContext
+        context: Context
+    ) = NetworkConnectionInterceptor(context)
 
     @Singleton
     @Provides
     fun providesOkHttpClient(
-        cacheInterceptor: CacheInterceptor,
+        networkConnectionInterceptor: NetworkConnectionInterceptor,
+        userAgentInterceptor: UserAgentInterceptor,
+        httpLoggingInterceptor: HttpLoggingInterceptor,
+//        cacheInterceptor: CacheInterceptor,
 //        adBlockInterceptor: AdBlockInterceptor,
-        httpLoggingInterceptor: HttpLoggingInterceptor
     ): OkHttpClient = OkHttpClient.Builder().apply {
+        addNetworkInterceptor(networkConnectionInterceptor)
+        addNetworkInterceptor(userAgentInterceptor)
 //        addNetworkInterceptor(adBlockInterceptor)
-        addNetworkInterceptor(cacheInterceptor)
+//        addNetworkInterceptor(cacheInterceptor)
         if (BuildConfig.DEBUG) {
             addInterceptor(httpLoggingInterceptor)
         }

+ 16 - 120
app/src/main/kotlin/org/mjdev/tvapp/sync/SyncAdapter.kt

@@ -16,32 +16,34 @@ import android.content.SyncResult
 import android.os.Bundle
 import androidx.annotation.Keep
 import io.objectbox.Box
+import org.mjdev.tvapp.BuildConfig
 import org.mjdev.tvapp.data.local.Movie
 import org.mjdev.tvapp.database.DAO
-import org.mjdev.tvapp.database.DAO.Companion.tx
-import org.mjdev.tvapp.database.DAO.Companion.with
 import org.mjdev.tvapp.repository.ApiService
+import org.mjdev.tvapp.sync.base.Syncer
+import org.mjdev.tvapp.sync.syncers.GithubMoviesSyncer
+import org.mjdev.tvapp.sync.syncers.WebMovieSyncer
 import org.mjdev.tvlib.extensions.GlobalExt.launchIO
-import org.mjdev.tvlib.extensions.GlobalExt.safeGet
-import org.mjdev.tvlib.webscrapper.adblock.AdBlock
-import org.mjdev.tvlib.interfaces.ItemWithUri.Companion.hasUri
-import org.mjdev.tvlib.webscrapper.WebVideoScrapper
-import timber.log.Timber
 
 @Keep
 @Suppress("unused")
 class SyncAdapter(
     context: Context,
     val apiService: ApiService,
-    val dao: DAO
+    val dao: DAO,
+    val movieDao: Box<Movie> = dao.movieDao,
 ) : AbstractThreadedSyncAdapter(context, true, false) {
 
-    private val movieDao: Box<Movie> get() = dao.movieDao
+    private fun ifDebug(syncer: Syncer) = if (BuildConfig.DEBUG) syncer else null
+    private fun ifNotDebug(syncer: Syncer) = if (!BuildConfig.DEBUG) syncer else null
 
-    private val urls = listOf(
-        "https://scartissue.site/",
-        "https://prehraj.to/hledej/filmy",
-    )
+    private val customSyncers: List<Syncer> by lazy {
+        listOfNotNull(
+            ifNotDebug(GithubMoviesSyncer(this)),
+            ifDebug(WebMovieSyncer(this, "https://tekeye.uk/html/html5-video-test-page")),
+            ifDebug(WebMovieSyncer(this, "https://prehraj.to/hledej/filmy")),
+        )
+    }
 
     override fun onPerformSync(
         account: Account?,
@@ -51,114 +53,8 @@ class SyncAdapter(
         syncResult: SyncResult?
     ) {
         launchIO {
-            val streams = apiService.streams().safeGet()
-            val channels = apiService.channels().safeGet()
-
-            val oldMovies = movieDao.all
-            val newMovies = mutableListOf<Movie>()
-
-            streams.forEach { stream ->
-                channels.firstOrNull { channel ->
-                    channel.id == stream.channelId
-                }.also { channel ->
-                    (channel?.categories ?: listOf(null)).forEach { ctg ->
-                        newMovies.add(Movie().apply {
-                            title = channel?.name ?: stream.channelId
-                            uri = stream.url
-                            category = ctg
-                            image = channel?.logo
-                            country = channel?.country
-                            isNsfw = channel?.isNsfw ?: false
-                        })
-                    }
-                }
-            }
-
-            with(SyncItems(oldMovies, newMovies)) {
-                toAdd.forEach { movie ->
-                    try {
-                        dao.movieDao.tx {
-                            put(movie)
-                        }
-                        Timber.d("Movie: $movie stored.")
-                    } catch (e: Exception) {
-                        Timber.e(e)
-                    }
-                }
-                toUpdate.forEach { movie ->
-                    try {
-                        dao.movieDao.tx {
-                            put(movie)
-                        }
-                        Timber.d("Movie: $movie updated.")
-                    } catch (e: Exception) {
-                        Timber.e(e)
-                    }
-                }
-                toRemove.forEach { movie ->
-                    try {
-                        dao.movieDao.tx {
-                            remove(movie)
-                        }
-                        Timber.d("Movie: $movie removed.")
-                    } catch (e: Exception) {
-                        Timber.e(e)
-                    }
-                }
-            }
-
-            WebVideoScrapper(
-                context = context,
-                addBlock = AdBlock(context),
-                baseUrls = urls,
-                onVideoFound = { video ->
-                    with(Movie()) {
-                        category = video.category
-                        title = video.title
-                        image = video.thumb
-                        uri = video.url
-                    }.also { movie ->
-                        dao.movieDao.put(movie)
-                    }
-                },
-                onFinish = { videos ->
-                    Timber.d("Got new movies from scrapper: ${videos.size}")
-                }
-            ).start()
-
+            customSyncers.forEach { syncer -> syncer.sync() }
         }
     }
 }
 
-class SyncItems(
-    private val oldMovies: List<Movie>,
-    private val newMovies: List<Movie>
-) {
-    val toAdd = mutableListOf<Movie>()
-    val toUpdate = mutableListOf<Movie>()
-    val toRemove = mutableListOf<Movie>()
-
-    init {
-        newMovies.filter { newMovie ->
-            newMovie.hasUri && (oldMovies.count { oldMovie ->
-                oldMovie.uri.contentEquals(newMovie.uri)
-            } == 0)
-        }.also { addMovies ->
-            toAdd.addAll(addMovies)
-        }
-        newMovies.filter { newMovie ->
-            newMovie.hasUri && (oldMovies.count { oldMovie ->
-                oldMovie.uri.contentEquals(newMovie.uri)
-            } > 0)
-        }.also { updateMovies ->
-            toUpdate.addAll(updateMovies)
-        }
-        oldMovies.filter { oldMovie ->
-            (newMovies.count { newMovie ->
-                newMovie.uri.contentEquals(oldMovie.uri)
-            } == 0)
-        }.also { addMovies ->
-            toRemove.addAll(addMovies)
-        }
-    }
-}

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/sync/Authenticator.kt → app/src/main/kotlin/org/mjdev/tvapp/sync/auth/Authenticator.kt

@@ -1,4 +1,4 @@
-package org.mjdev.tvapp.sync
+package org.mjdev.tvapp.sync.auth
 
 import android.accounts.AbstractAccountAuthenticator
 import android.accounts.Account

+ 9 - 1
app/src/main/kotlin/org/mjdev/tvapp/sync/AuthenticatorService.kt → app/src/main/kotlin/org/mjdev/tvapp/sync/auth/AuthenticatorService.kt

@@ -1,4 +1,12 @@
-package org.mjdev.tvapp.sync
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.sync.auth
 
 import android.app.Service
 import android.content.Intent

+ 46 - 0
app/src/main/kotlin/org/mjdev/tvapp/sync/base/SyncItems.kt

@@ -0,0 +1,46 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.sync.base
+
+import org.mjdev.tvapp.data.local.Movie
+import org.mjdev.tvlib.interfaces.ItemWithUri.Companion.hasUri
+
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+class SyncItems(
+    private val oldMovies: List<Movie>,
+    private val newMovies: List<Movie>
+) {
+    val toAdd = mutableListOf<Movie>()
+    val toUpdate = mutableListOf<Movie>()
+    val toRemove = mutableListOf<Movie>()
+
+    init {
+        newMovies.filter { newMovie ->
+            newMovie.hasUri && (oldMovies.count { oldMovie ->
+                oldMovie.uri.contentEquals(newMovie.uri)
+            } == 0)
+        }.also { addMovies ->
+            toAdd.addAll(addMovies)
+        }
+        newMovies.filter { newMovie ->
+            newMovie.hasUri && (oldMovies.count { oldMovie ->
+                oldMovie.uri.contentEquals(newMovie.uri)
+            } > 0)
+        }.also { updateMovies ->
+            toUpdate.addAll(updateMovies)
+        }
+        oldMovies.filter { oldMovie ->
+            (newMovies.count { newMovie ->
+                newMovie.uri.contentEquals(oldMovie.uri)
+            } == 0)
+        }.also { addMovies ->
+            toRemove.addAll(addMovies)
+        }
+    }
+}

+ 24 - 0
app/src/main/kotlin/org/mjdev/tvapp/sync/base/Syncer.kt

@@ -0,0 +1,24 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.sync.base
+
+import android.content.Context
+import org.mjdev.tvapp.sync.SyncAdapter
+
+abstract class Syncer(
+    private val adapter: SyncAdapter
+) {
+    val context: Context get() = adapter.context
+    val dao get() = adapter.dao
+    val apiService get() = adapter.apiService
+    val movieDao get() = adapter.movieDao
+
+    abstract suspend fun sync()
+
+}

+ 9 - 1
app/src/main/kotlin/org/mjdev/tvapp/sync/DataProvider.kt → app/src/main/kotlin/org/mjdev/tvapp/sync/provider/DataProvider.kt

@@ -1,4 +1,12 @@
-package org.mjdev.tvapp.sync
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.sync.provider
 
 import android.content.ContentProvider
 import android.content.ContentValues

+ 72 - 0
app/src/main/kotlin/org/mjdev/tvapp/sync/syncers/GithubMoviesSyncer.kt

@@ -0,0 +1,72 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.sync.syncers
+
+import org.mjdev.tvapp.data.local.Movie
+import org.mjdev.tvapp.sync.SyncAdapter
+import org.mjdev.tvapp.sync.base.SyncItems
+import org.mjdev.tvapp.sync.base.Syncer
+import org.mjdev.tvlib.database.extensions.ObjectBoxExt.stx
+import org.mjdev.tvlib.database.extensions.ObjectBoxExt.update
+import org.mjdev.tvlib.extensions.ApiResponseExt.safeGet
+import timber.log.Timber
+
+class GithubMoviesSyncer(adapter: SyncAdapter) : Syncer(adapter) {
+    override suspend fun sync() {
+        val streams = apiService.streams().safeGet()
+        val channels = apiService.channels().safeGet()
+
+        val oldMovies = movieDao.all
+        val newMovies = mutableListOf<Movie>()
+
+        streams.forEach { stream ->
+            channels.firstOrNull { channel ->
+                channel.id == stream.channelId
+            }.also { channel ->
+                (channel?.categories ?: listOf(null)).forEach { ctg ->
+                    newMovies.add(Movie().apply {
+                        title = channel?.name ?: stream.channelId
+                        uri = stream.url
+                        category = ctg
+                        image = channel?.logo
+                        country = channel?.country
+                        isNsfw = channel?.isNsfw ?: false
+                    })
+                }
+            }
+        }
+
+        with(SyncItems(oldMovies, newMovies)) {
+            toAdd.forEach { movie ->
+                try {
+                    dao.movieDao.stx { put(movie) }
+                    Timber.d("Movie: $movie stored.")
+                } catch (e: Exception) {
+                    Timber.e(e)
+                }
+            }
+            toUpdate.forEach { movie ->
+                try {
+                    dao.movieDao.stx { update(movie) { m -> m.uri == movie.uri } }
+                    Timber.d("Movie: $movie updated.")
+                } catch (e: Exception) {
+                    Timber.e(e)
+                }
+            }
+            toRemove.forEach { movie ->
+                try {
+                    dao.movieDao.stx { remove(movie) }
+                    Timber.d("Movie: $movie removed.")
+                } catch (e: Exception) {
+                    Timber.e(e)
+                }
+            }
+        }
+    }
+}

+ 42 - 0
app/src/main/kotlin/org/mjdev/tvapp/sync/syncers/WebMovieSyncer.kt

@@ -0,0 +1,42 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.sync.syncers
+
+import org.mjdev.tvapp.data.local.Movie
+import org.mjdev.tvapp.sync.SyncAdapter
+import org.mjdev.tvapp.sync.base.Syncer
+import org.mjdev.tvlib.database.extensions.ObjectBoxExt.tx
+import org.mjdev.tvlib.database.extensions.ObjectBoxExt.update
+import org.mjdev.tvlib.webscrapper.WebVideoScrapper
+import org.mjdev.tvlib.webscrapper.adblock.AdBlock
+
+class WebMovieSyncer(
+    adapter: SyncAdapter,
+    private val url: String
+) : Syncer(adapter) {
+    override suspend fun sync() {
+        WebVideoScrapper(
+            context = context,
+            addBlock = AdBlock(context),
+            baseUrls = listOf(url),
+            onVideoFound = { video ->
+                Movie().apply {
+                    category = video.category
+                    title = video.title
+                    image = video.thumb
+                    uri = video.url
+                }.also { movie ->
+                    dao.movieDao.tx {
+                        update(movie) { m -> m.uri == video.url }
+                    }
+                }
+            }
+        ).start()
+    }
+}

+ 0 - 2
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt

@@ -21,7 +21,6 @@ import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.screen.ScreenWithPages
 import org.mjdev.tvlib.ui.components.page.PagerScope
 import org.mjdev.tvlib.ui.components.page.SearchPage
-import org.mjdev.tvlib.ui.components.page.SettingsPage
 
 class MainScreen : ScreenWithPages() {
 
@@ -37,7 +36,6 @@ class MainScreen : ScreenWithPages() {
         page(SubscriptionPage())
         page(PluginsPage())
 
-        page(SettingsPage(), menuGravity = Bottom)
         page(AboutPage(), menuGravity = Bottom)
 
     }

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt

@@ -26,7 +26,7 @@ import org.mjdev.tvlib.helpers.cursor.VideoCursor
 import org.mjdev.tvlib.viewmodel.BaseViewModel
 import org.mjdev.tvapp.data.local.Movie
 import org.mjdev.tvapp.database.DAO
-import org.mjdev.tvapp.database.DAO.Companion.findById
+import org.mjdev.tvlib.database.extensions.ObjectBoxExt.findById
 import org.mjdev.tvlib.extensions.ListExt.asMap
 import org.mjdev.tvlib.extensions.ListExt.takeIf
 import org.mjdev.tvlib.helpers.apps.appsManager

+ 0 - 24
buildSrc/build.gradle.kts

@@ -33,33 +33,9 @@ dependencies {
     implementation("com.android.tools.build:gradle:8.2.0")
     implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
     implementation("com.squareup:javapoet:1.13.0")
-
 //    implementation("com.bmuschko:gradle-docker-plugin:9.4.0")
 //    implementation("org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18")
 //    implementation(kotlin("reflect", "1.8.21"))
-
-//    implementation("com.twelvemonkeys.imageio:imageio-core:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-webp:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-png:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-bmp:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-jpeg:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-tiff:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-batik:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-metadata:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-psd:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-icns:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-pnm:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-sgi:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-pict:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-tga:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-pcx:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-iff:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-pdf:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-pnm:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-hdr:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-clippath:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-thumbsdb:3.9.4")
-//    implementation("com.twelvemonkeys.imageio:imageio-xwd:3.9.4")
 //    implementation("com.soywiz.korlibs.korim:korim-jvm:4.0.2")
 //    implementation("com.github.mukeshsolanki:photofilter:2.0.2")
 //    implementation("gradle.plugin.com.autonomousapps:dependency-analysis-gradle-plugin:0.10.0")

+ 35 - 5
buildSrc/src/main/kotlin/org/mjdev/gradle/dependency/DependencyHandler.kt

@@ -198,6 +198,13 @@ fun DependencyHandler.widgetDependencies() = arrayOf(
     implementation("androidx.glance:glance-material3:1.0.0"),
 )
 
+fun DependencyHandler.stripeDependencies() = arrayOf(
+    // stripe payments
+    implementation("com.stripe:stripe-android:20.33.0"),
+    implementation("com.stripe:stripecardscan:20.33.0"),
+    implementation("com.stripe:financial-connections:20.33.0"),
+)
+
 fun DependencyHandler.anotherDependencies() = arrayOf(
     // zxing
     implementation("com.google.zxing:core:3.5.2"),
@@ -215,12 +222,10 @@ fun DependencyHandler.anotherDependencies() = arrayOf(
     implementation("com.dailymotion.dailymotion-sdk-android:sdk:0.2.12"),
     // dynamic theme
     implementation("com.google.android.material:material:1.11.0"),
-    // stripe payments
-    implementation("com.stripe:stripe-android:20.33.0"),
-    implementation("com.stripe:stripecardscan:20.33.0"),
-    implementation("com.stripe:financial-connections:20.33.0"),
     // scrape
     implementation("net.sourceforge.htmlunit:htmlunit-android:2.67.0"),
+//    implementation("org.htmlunit:htmlunit3-android:3.7.0"),
+//    implementation("org.htmlunit:htmlunit:3.9.0"),
     // flip card
     implementation("com.wajahatkarim:flippable:1.5.4"),
     // ui controller
@@ -239,4 +244,29 @@ fun DependencyHandler.anotherDependencies() = arrayOf(
 
 fun DependencyHandler.mjdevTvLib() = arrayOf(
     implementation(project(mapOf("path" to ":tvlib")))
-)
+)
+
+//fun DependencyHandler.imageIODependencies() = arrayOf(
+//    implementation("com.twelvemonkeys.imageio:imageio-core:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-webp:3.9.4"),
+////    implementation("com.twelvemonkeys.imageio:imageio-png:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-bmp:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-jpeg:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-tiff:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-batik:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-metadata:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-psd:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-icns:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-pnm:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-sgi:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-pict:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-tga:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-pcx:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-iff:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-pdf:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-pnm:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-hdr:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-clippath:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-thumbsdb:3.9.4"),
+//    implementation("com.twelvemonkeys.imageio:imageio-xwd:3.9.4"),
+//)

+ 37 - 35
buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/SigningConfigs.kt

@@ -6,41 +6,43 @@
  *  w: https://mjdev.org
  */
 
+@file:Suppress("unused")
+
 package org.mjdev.gradle.extensions
 
-//import com.android.build.gradle.BaseExtension
-//import org.gradle.api.Project
-//import java.io.FileInputStream
-//import java.util.*
-//import kotlin.String
+import com.android.build.gradle.BaseExtension
+import org.gradle.api.Project
+import java.io.FileInputStream
+import java.util.*
+import kotlin.String
+
+fun BaseExtension.setSigningConfigs(
+    project: Project
+) = signingConfigs {
+    create("signingConfigRelease") {
+        val rootProject = project.rootProject
+        val keystorePropertiesFile = rootProject.file("signing/release.signing.properties")
+        if (!keystorePropertiesFile.exists()) {
+            System.err.println("📜 Missing release.signing.properties file for release signing")
+        } else {
+            val keystoreProperties = Properties().apply {
+                load(FileInputStream(keystorePropertiesFile))
+            }
+            try {
+                storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
+                storePassword = keystoreProperties["storePassword"] as String
+                keyAlias = keystoreProperties["keyAlias"] as String
+                keyPassword = keystoreProperties["keyPassword"] as String
+            } catch (e: Exception) {
+                System.err.println("📜 release.signing.properties file is malformed")
+            }
+        }
+    }
 
-//    fun BaseExtension.setSigningConfigs(
-//        project: Project
-//    ) = signingConfigs {
-//        create("signingConfigRelease") {
-//            val rootProject = project.rootProject
-//            val keystorePropertiesFile = rootProject.file("signing/release.signing.properties")
-//            if (!keystorePropertiesFile.exists()) {
-//                System.err.println("📜 Missing release.signing.properties file for release signing")
-//            } else {
-//                val keystoreProperties = Properties().apply {
-//                    load(FileInputStream(keystorePropertiesFile))
-//                }
-//                try {
-//                    storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
-//                    storePassword = keystoreProperties["storePassword"] as String
-//                    keyAlias = keystoreProperties["keyAlias"] as String
-//                    keyPassword = keystoreProperties["keyPassword"] as String
-//                } catch (e: Exception) {
-//                    System.err.println("📜 release.signing.properties file is malformed")
-//                }
-//            }
-//        }
-//
-//        getByName("debug") {
-//            storeFile = project.rootProject.file("signing/debug.keystore")
-//            keyAlias = "androiddebugkey"
-//            keyPassword = "android"
-//            storePassword = "android"
-//        }
-//    }
+    getByName("debug") {
+        storeFile = project.rootProject.file("signing/debug.keystore")
+        keyAlias = "androiddebugkey"
+        keyPassword = "android"
+        storePassword = "android"
+    }
+}

+ 11 - 8
buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/TestedExtension.kt

@@ -6,14 +6,17 @@
  *  w: https://mjdev.org
  */
 
+@file:Suppress("unused")
+
 package org.mjdev.gradle.extensions
 
-//import com.android.build.gradle.AppExtension
-//import com.android.build.gradle.LibraryExtension
-//import com.android.build.gradle.TestedExtension
+import com.android.build.gradle.AppExtension
+import com.android.build.gradle.LibraryExtension
+import com.android.build.gradle.TestedExtension
+import org.gradle.api.GradleException
 
-//    fun TestedExtension.variants() = when (this) {
-//        is AppExtension -> applicationVariants
-//        is LibraryExtension -> libraryVariants
-//        else -> throw GradleException("Unsupported BaseExtension type!")
-//    }
+fun TestedExtension.variants() = when (this) {
+    is AppExtension -> applicationVariants
+    is LibraryExtension -> libraryVariants
+    else -> throw GradleException("Unsupported BaseExtension type!")
+}

+ 1 - 1
config/version.properties

@@ -9,4 +9,4 @@
 # suppress inspection "UnusedProperty" for whole file
 majorVersion=1
 minorVersion=2
-patchVersion=1
+patchVersion=2

+ 3 - 1
release-notes.txt

@@ -1,4 +1,6 @@
-* Release Notes 1.2.1
+* Release Notes 1.2.2
+* config field fix / proguard fix
+* build improvements
 * scrapper base
 * navigation improvements
 * navigation improvements / need more improvements

+ 5 - 3
tvlib/build.gradle.kts

@@ -33,6 +33,7 @@ plugins {
     id("com.google.dagger.hilt.android")
     id("dagger.hilt.android.plugin")
     id("com.google.devtools.ksp")
+    id("io.objectbox")
 //    kotlinAndroid()
 //    kotlinKapt()
 //    androidLibrary()
@@ -123,6 +124,8 @@ android {
 
 dependencies {
     baseDependencies()
+    timberDependencies()
+    daggerDependencies()
     composeDependencies()
     okHttpDependencies()
     retrofitDependencies()
@@ -130,11 +133,10 @@ dependencies {
     sandwichDependencies()
     glideDependencies()
     oauthDependencies()
-    timberDependencies()
-    daggerDependencies()
     exoPlayerDependencies()
-    testDependencies()
     encryptDependencies()
     permissionsDependencies()
+//    imageIODependencies()
     anotherDependencies()
+    testDependencies()
 }

+ 32 - 32
tvlib/src/main/kotlin/org/mjdev/tvlib/database/convert/CustomPropertyConverter.kt

@@ -1,32 +1,32 @@
-///*
-// *  Copyright (c) Milan Jurkulák 2023.
-// *  Contact:
-// *  e: mimoccc@gmail.com
-// *  e: mj@mjdev.org
-// *  w: https://mjdev.org
-// */
-//
-//@file:Suppress("unused")
-//
-//package org.mjdev.tvlib.database.convert
-//
-//import io.objectbox.converter.PropertyConverter
-//
-//open class ConvertEntity : Any() {
-//    open val id: Long = 0L
-//}
-//
-//open class CustomPropertyConverter<T : ConvertEntity>(
-//
-//    private val supplier: (id: Long) -> T?
-//
-//) : PropertyConverter<T, Long> {
-//
-//    override fun convertToEntityProperty(databaseValue: Long?): T? =
-//        if (databaseValue == null) null
-//        else supplier.invoke(databaseValue)
-//
-//    override fun convertToDatabaseValue(entityProperty: T): Long =
-//        entityProperty.id
-//
-//}
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("unused")
+
+package org.mjdev.tvlib.database.convert
+
+import io.objectbox.converter.PropertyConverter
+
+open class ConvertEntity : Any() {
+    open val id: Long = 0L
+}
+
+open class CustomPropertyConverter<T : ConvertEntity>(
+
+    private val supplier: (id: Long) -> T?
+
+) : PropertyConverter<T, Long> {
+
+    override fun convertToEntityProperty(databaseValue: Long?): T? =
+        if (databaseValue == null) null
+        else supplier.invoke(databaseValue)
+
+    override fun convertToDatabaseValue(entityProperty: T): Long =
+        entityProperty.id
+
+}

+ 30 - 30
tvlib/src/main/kotlin/org/mjdev/tvlib/database/convert/EncryptionConverter.kt

@@ -1,30 +1,30 @@
-///*
-// *  Copyright (c) Milan Jurkulák 2023.
-// *  Contact:
-// *  e: mimoccc@gmail.com
-// *  e: mj@mjdev.org
-// *  w: https://mjdev.org
-// */
-//
-//@file:Suppress("unused")
-//
-//package org.mjdev.tvlib.database.convert
-//
-//import com.scottyab.aescrypt.AESCrypt
-//import io.objectbox.converter.PropertyConverter
-//
-//class EncryptionConverter : PropertyConverter<String, String> {
-//
-//    companion object {
-//
-//        const val AES_KEY = "simply key to encrypt data"
-//
-//    }
-//
-//    override fun convertToDatabaseValue(entityProperty: String): String =
-//        AESCrypt.encrypt(AES_KEY, entityProperty)
-//
-//    override fun convertToEntityProperty(databaseValue: String?): String =
-//        AESCrypt.decrypt(AES_KEY, databaseValue)
-//
-//}
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("unused")
+
+package org.mjdev.tvlib.database.convert
+
+import com.scottyab.aescrypt.AESCrypt
+import io.objectbox.converter.PropertyConverter
+
+class EncryptionConverter : PropertyConverter<String, String> {
+
+    companion object {
+
+        const val AES_KEY = "simply key to encrypt data"
+
+    }
+
+    override fun convertToDatabaseValue(entityProperty: String): String =
+        AESCrypt.encrypt(AES_KEY, entityProperty)
+
+    override fun convertToEntityProperty(databaseValue: String?): String =
+        AESCrypt.decrypt(AES_KEY, databaseValue)
+
+}

+ 23 - 23
tvlib/src/main/kotlin/org/mjdev/tvlib/database/convert/PropertyEnumConverter.kt

@@ -6,26 +6,26 @@
  *  w: https://mjdev.org
  */
 
-//package org.mjdev.tvlib.database.convert
-//
-//import io.objectbox.converter.PropertyConverter
-//
-//@Suppress("unused")
-//open class PropertyEnumConverter<T, R>(
-//
-//    private val values: Array<T>,
-//    private val default: T,
-//    private val predicate: T.(R) -> Boolean,
-//    private val supplier: (T) -> R?
-//
-//) : PropertyConverter<T, R> {
-//
-//    override fun convertToEntityProperty(databaseValue: R?): T? =
-//        if (databaseValue == null) null
-//        else values.firstOrNull { p -> p.predicate(databaseValue) } ?: default
-//
-//    override fun convertToDatabaseValue(entityProperty: T?): R? =
-//        if (entityProperty == null) null
-//        else supplier(entityProperty)
-//
-//}
+package org.mjdev.tvlib.database.convert
+
+import io.objectbox.converter.PropertyConverter
+
+@Suppress("unused")
+open class PropertyEnumConverter<T, R>(
+
+    private val values: Array<T>,
+    private val default: T,
+    private val predicate: T.(R) -> Boolean,
+    private val supplier: (T) -> R?
+
+) : PropertyConverter<T, R> {
+
+    override fun convertToEntityProperty(databaseValue: R?): T? =
+        if (databaseValue == null) null
+        else values.firstOrNull { p -> p.predicate(databaseValue) } ?: default
+
+    override fun convertToDatabaseValue(entityProperty: T?): R? =
+        if (entityProperty == null) null
+        else supplier(entityProperty)
+
+}

+ 21 - 21
tvlib/src/main/kotlin/org/mjdev/tvlib/database/convert/PropertyJsonConverter.kt

@@ -6,24 +6,24 @@
  *  w: https://mjdev.org
  */
 
-//package org.mjdev.tvlib.database.convert
-//
-//import com.squareup.moshi.Moshi
-//import io.objectbox.converter.PropertyConverter
-//
-//@Suppress("unused")
-//open class PropertyJsonConverter<T : Any>(
-//
-//    private val cls: Class<T>
-//
-//    ) : PropertyConverter<T, String> {
-//
-//    override fun convertToEntityProperty(databaseValue: String?): T? =
-//        if (databaseValue == null) null
-//        else Moshi.Builder().build().adapter(cls).fromJson(databaseValue) as T
-//
-//    override fun convertToDatabaseValue(entityProperty: T?): String? =
-//        if (entityProperty == null) null
-//        else Moshi.Builder().build().adapter(cls).toJson(entityProperty)
-//
-//}
+package org.mjdev.tvlib.database.convert
+
+import com.squareup.moshi.Moshi
+import io.objectbox.converter.PropertyConverter
+
+@Suppress("unused")
+open class PropertyJsonConverter<T : Any>(
+
+    private val cls: Class<T>
+
+    ) : PropertyConverter<T, String> {
+
+    override fun convertToEntityProperty(databaseValue: String?): T? =
+        if (databaseValue == null) null
+        else Moshi.Builder().build().adapter(cls).fromJson(databaseValue) as T
+
+    override fun convertToDatabaseValue(entityProperty: T?): String? =
+        if (entityProperty == null) null
+        else Moshi.Builder().build().adapter(cls).toJson(entityProperty)
+
+}

+ 33 - 33
tvlib/src/main/kotlin/org/mjdev/tvlib/database/convert/StringListConverter.kt

@@ -6,36 +6,36 @@
  *  w: https://mjdev.org
  */
 
-//package org.mjdev.tvlib.database.convert
-//
-//import io.objectbox.converter.PropertyConverter
-//import org.json.JSONArray
-//
-//@Suppress("unused")
-//class StringListConverter : PropertyConverter<List<String?>?, String?> {
-//
-//    override fun convertToEntityProperty(
-//        databaseValue: String?
-//    ): List<String?> = if (databaseValue == null) ArrayList()
-//    else try {
-//        val array = JSONArray(databaseValue)
-//        val ret = ArrayList<String?>()
-//        for (i in 0 until array.length()) {
-//            ret.add(array.getString(i))
-//        }
-//        ret
-//    } catch (e: Exception) {
-//        ArrayList()
-//    }
-//
-//    override fun convertToDatabaseValue(
-//        entityProperty: List<String?>?
-//    ): String? {
-//        return try {
-//            if (entityProperty == null) null else JSONArray(entityProperty).toString()
-//        } catch (e: Exception) {
-//            null
-//        }
-//    }
-//
-//}
+package org.mjdev.tvlib.database.convert
+
+import io.objectbox.converter.PropertyConverter
+import org.json.JSONArray
+
+@Suppress("unused")
+class StringListConverter : PropertyConverter<List<String?>?, String?> {
+
+    override fun convertToEntityProperty(
+        databaseValue: String?
+    ): List<String?> = if (databaseValue == null) ArrayList()
+    else try {
+        val array = JSONArray(databaseValue)
+        val ret = ArrayList<String?>()
+        for (i in 0 until array.length()) {
+            ret.add(array.getString(i))
+        }
+        ret
+    } catch (e: Exception) {
+        ArrayList()
+    }
+
+    override fun convertToDatabaseValue(
+        entityProperty: List<String?>?
+    ): String? {
+        return try {
+            if (entityProperty == null) null else JSONArray(entityProperty).toString()
+        } catch (e: Exception) {
+            null
+        }
+    }
+
+}

+ 53 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/database/extensions/ObjectBoxExt.kt

@@ -0,0 +1,53 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.database.extensions
+
+import io.objectbox.Box
+import io.objectbox.kotlin.awaitCallInTx
+import org.mjdev.tvlib.extensions.GlobalExt.launchIO
+
+object ObjectBoxExt {
+
+    suspend inline fun <reified V, T> Box<T>.stx(
+        crossinline transaction: Box<T>.() -> V
+    ) = store.awaitCallInTx {
+        transaction(this)
+    }
+
+    inline fun <reified V, T> Box<T>.tx(
+        crossinline transaction: Box<T>.() -> V
+    ) = launchIO {
+        store.awaitCallInTx {
+            transaction(this@tx)
+        }
+    }
+
+    // todo remove reflection && improve search
+    @Suppress("unused")
+    inline fun <reified T> Box<T>.findById(id: Long?): T? = if (id == null) all.firstOrNull()
+    else all.firstOrNull { o -> o.property("id") == id }
+
+    // todo remove reflection
+    inline fun <reified T> T.property(name: String): Any? = T::class.members.firstOrNull {
+        it.name == name
+    }?.call(this)
+
+    fun <T : Any> Box<T>.update(entity: T, query: (T) -> Boolean) {
+        val exists = query(query)
+        if (exists.isNotEmpty()) {
+            remove(exists)
+        }
+        put(entity)
+    }
+
+    // todo improve for faster search
+    @Suppress("MemberVisibilityCanBePrivate")
+    fun <T : Any> Box<T>.query(query: (T) -> Boolean) = all.filter(query)
+
+}

+ 14 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/exception/CodeException.kt

@@ -0,0 +1,14 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.exception
+
+class CodeException(
+    message: String = "Unhandled Exception.",
+    code: Int = -1,
+) : Exception(String.format("$code : $message."))

+ 55 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ApiResponseExt.kt

@@ -0,0 +1,55 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.extensions
+
+import com.skydoves.sandwich.ApiResponse
+import com.skydoves.sandwich.getOrNull
+import com.skydoves.sandwich.message
+import com.skydoves.sandwich.onError
+import com.skydoves.sandwich.onException
+import com.skydoves.sandwich.toFlow
+import kotlinx.coroutines.flow.Flow
+import org.mjdev.tvlib.exception.CodeException
+import timber.log.Timber
+
+@Suppress("unused")
+object ApiResponseExt {
+
+    fun <T> ApiResponse<T>.safeFlow(
+        error: (exception: Exception) -> Unit = { exception ->
+            Timber.e(exception)
+        },
+        onError: (error: ApiResponse.Failure.Error) -> Unit = { e ->
+            error(CodeException(e.message()))
+        }
+    ): Flow<T> = onException {
+        error(this)
+    }.onError {
+        onError(this)
+    }.toFlow()
+
+    inline fun <reified T> ApiResponse<List<T>>.safeGet(
+        crossinline error: (exception: Exception) -> Unit = { exception ->
+            Timber.e(exception)
+        }
+    ): List<T> = onException {
+        error(this)
+    }.onError {
+        error(CodeException(message()))
+    }.getOrNull() ?: emptyList()
+
+    suspend fun <E> runSafe(block: suspend () -> E): Result<E> = try {
+        block.invoke().let { result ->
+            Result.success(result)
+        }
+    } catch (e: Exception) {
+        Result.failure(e)
+    }
+
+}

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ColorExt.kt

@@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.ColorMatrix
 import kotlin.math.roundToInt
 
+@Suppress("unused")
 object ColorExt {
 
     fun Color.invert(): Color {

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ContextExt.kt

@@ -18,6 +18,7 @@ import android.text.format.DateFormat
 import timber.log.Timber
 import java.util.Date
 
+@Suppress("unused", "UnusedReceiverParameter")
 object ContextExt {
 
     val Context.isEink: Boolean

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/CursorExt.kt

@@ -16,6 +16,7 @@ import androidx.core.database.getStringOrNull
 import org.mjdev.tvlib.helpers.cursor.CachingCursor
 import timber.log.Timber
 
+@Suppress("unused")
 object CursorExt {
 
     val Cursor.isNotEmpty get() = count > 0

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/DrawableExt.kt

@@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.asImageBitmap
 import androidx.core.graphics.drawable.toBitmap
 import org.mjdev.tvlib.ui.drawable.PhotoDrawable
 
+@Suppress("unused")
 object DrawableExt {
 
     // todo check width and height

+ 6 - 54
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt

@@ -11,24 +11,14 @@ package org.mjdev.tvlib.extensions
 import android.os.Handler
 import android.os.Looper
 import androidx.compose.runtime.MutableState
-import com.skydoves.sandwich.ApiResponse
-import com.skydoves.sandwich.getOrNull
-import com.skydoves.sandwich.message
-import com.skydoves.sandwich.onError
-import com.skydoves.sandwich.onException
-import com.skydoves.sandwich.toFlow
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.launch
-import timber.log.Timber
-import java.net.MalformedURLException
-import java.net.URL
 import kotlin.coroutines.CoroutineContext
 
-@Suppress("unused")
+@Suppress("unused", "MemberVisibilityCanBePrivate")
 object GlobalExt {
 
     val UI = Dispatchers.Main
@@ -44,11 +34,11 @@ object GlobalExt {
         block = block
     )
 
-    fun launchUI (
+    fun launchUI(
         block: suspend CoroutineScope.() -> Unit
     ) = launch(UI) { block() }
 
-    fun launchIO (
+    fun launchIO(
         block: suspend CoroutineScope.() -> Unit
     ) = launch(IO) { block() }
 
@@ -59,51 +49,13 @@ object GlobalExt {
         block.invoke(this)
     }, delay)
 
-    class CodeException(
-        message: String = "Unhandled Exception.",
-        code: Int = -1,
-    ) : Exception(String.format("$code : $message."))
-
     fun MutableState<Boolean>.toggle() {
         value = !value
     }
 
-    fun <T> ApiResponse<T>.safeFlow(
-        error: (exception: Exception) -> Unit = { exception ->
-            Timber.e(exception)
-        },
-        onError: (error: ApiResponse.Failure.Error) -> Unit = { e ->
-            error(CodeException(e.message()))
-        }
-    ): Flow<T> = onException {
-        error(this)
-    }.onError {
-        onError(this)
-    }.toFlow()
-
-    inline fun <reified T> ApiResponse<List<T>>.safeGet(
-        crossinline error: (exception: Exception) -> Unit = { exception ->
-            Timber.e(exception)
-        }
-    ): List<T> = onException {
-        error(this)
-    }.onError {
-        error(CodeException(message()))
-    }.getOrNull() ?: emptyList()
-
-    suspend fun <E> runSafe(block: suspend () -> E): Result<E> = try {
-        block.invoke().let { result ->
-            Result.success(result)
-        }
-    } catch (e: Exception) {
-        Result.failure(e)
+    inline fun <T> with(receiver: T, block: T.() -> Unit): T {
+        block.invoke(receiver)
+        return receiver
     }
 
-    @Suppress("SENSELESS_COMPARISON")
-    val String.isUrl: Boolean
-        get() = try {
-            URL(this) != null
-        } catch (e: MalformedURLException) {
-            false
-        }
 }

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/HiltExt.kt

@@ -34,6 +34,7 @@ import kotlin.reflect.KClass
 import kotlin.reflect.full.companionObject
 import kotlin.reflect.full.functions
 
+@Suppress("unused")
 object HiltExt {
 
     @Suppress("LocalVariableName")

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/LifecycleExt.kt

@@ -16,6 +16,7 @@ import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.LifecycleOwner
 
+@Suppress("unused")
 object LifecycleExt {
 
     @SuppressLint("ComposableNaming")

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/MediaItemExt.kt

@@ -24,6 +24,7 @@ import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.interfaces.ItemWithUri
 
+@Suppress("unused")
 object MediaItemExt {
 
     val Context.metadataRetriever

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavBackStackEntryExt.kt

@@ -10,6 +10,7 @@ package org.mjdev.tvlib.extensions
 
 import androidx.navigation.NavBackStackEntry
 
+@Suppress("unused")
 object NavBackStackEntryExt {
 
     @Suppress("UNCHECKED_CAST", "DEPRECATION")

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavExt.kt

@@ -25,6 +25,7 @@ import org.mjdev.tvlib.navigation.MenuItem
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
 import org.mjdev.tvlib.navigation.NavHostControllerEx
 
+@Suppress("unused")
 object NavExt {
 
     inline fun NavHostControllerEx.createGraph(

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavGraphBuilderExt.kt

@@ -20,6 +20,7 @@ import org.mjdev.tvlib.screen.Screen
 
 typealias AnimType<T> = (@JvmSuppressWildcards AnimatedContentTransitionScope<NavBackStackEntry>.() -> T?)
 
+@Suppress("unused")
 object NavGraphBuilderExt {
 
     fun <T : Screen> NavGraphBuilderEx.startScreen(

+ 19 - 18
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/OkHttpClientExt.kt

@@ -16,6 +16,7 @@ import okhttp3.OkHttpClient
 import okhttp3.Request
 import okhttp3.Response
 
+@Suppress("unused")
 object OkHttpClientExt {
 
     fun OkHttpClient.download(
@@ -37,23 +38,23 @@ object OkHttpClientExt {
         }
     }
 
-//    fun OkHttpClient.download(
-//        url: String,
-//        onResult: (Result<String>) -> Unit
-//    ) {
-//        Request.Builder().url(url).build().also { request ->
-//            newCall(request).enqueue(
-//                object : Callback {
-//                    override fun onFailure(call: Call, e: okio.IOException) {
-//                        onResult.invoke(Result.failure(e))
-//                    }
-//
-//                    override fun onResponse(call: Call, response: Response) {
-//                        onResult.invoke(Result.success(response.body.string()))
-//                    }
-//                }
-//            )
-//        }
-//    }
+    fun OkHttpClient.downloadString(
+        url: String,
+        onResult: (Result<String>) -> Unit
+    ) {
+        Request.Builder().url(url).build().also { request ->
+            newCall(request).enqueue(
+                object : Callback {
+                    override fun onFailure(call: Call, e: okio.IOException) {
+                        onResult.invoke(Result.failure(e))
+                    }
+
+                    override fun onResponse(call: Call, response: Response) {
+                        onResult.invoke(Result.success(response.body.string()))
+                    }
+                }
+            )
+        }
+    }
 
 }

+ 10 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/StringExt.kt

@@ -9,6 +9,8 @@
 package org.mjdev.tvlib.extensions
 
 import android.net.Uri
+import java.net.MalformedURLException
+import java.net.URL
 
 @Suppress("unused")
 object StringExt {
@@ -27,4 +29,12 @@ object StringExt {
 
     fun List<String>.trimLines() = map { line -> line.trim() }
 
+    @Suppress("SENSELESS_COMPARISON")
+    val String.isUrl: Boolean
+        get() = try {
+            URL(this) != null
+        } catch (e: MalformedURLException) {
+            false
+        }
+
 }

+ 1 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ViewExt.kt

@@ -63,6 +63,7 @@ object ViewExt {
     }
 
     val PlayerView.controller: PlayerControlView?
+        @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
         get() = allViews.firstOrNull { v -> v is PlayerControlView } as? PlayerControlView
 
     val PlayerView.buttons: List<ImageButton>

+ 10 - 43
tvlib/src/main/kotlin/org/mjdev/tvlib/glide/OkHttpUrlLoaderEx.kt

@@ -24,67 +24,34 @@ import org.mjdev.tvlib.BuildConfig
 import java.io.InputStream
 import java.util.concurrent.TimeUnit
 
-/**
- * Custom URL loader
- * This loader implements image load debugging availability
- * @property factory Factory
- * @property nonDebugClient OkHttpClient
- * @property debugClient OkHttpClient?
- * @property client OkHttpClient
- * @property context Context
- * @constructor
- */
 @Keep
 class OkHttpUrlLoaderEx(
     private val factory: Factory
 ) : ModelLoader<GlideUrl, InputStream>, SharedPreferences.OnSharedPreferenceChangeListener {
-    private val nonDebugClient: OkHttpClient = OkHttpClient.Builder().apply {
+
+    private val client: OkHttpClient = OkHttpClient.Builder().apply {
         readTimeout(30, TimeUnit.SECONDS)
         writeTimeout(30, TimeUnit.SECONDS)
         connectTimeout(30, TimeUnit.SECONDS)
         followRedirects(true)
         followSslRedirects(true)
+        addInterceptor(
+            HttpLoggingInterceptor().setLevel(
+                if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
+                else HttpLoggingInterceptor.Level.NONE
+            )
+        )
     }.build()
 
-    /**
-     * Client that make requests debug info
-     */
-    private var debugClient: OkHttpClient? = null
-
-    /**
-     * Standard client, not making any info
-     */
-    private val client: OkHttpClient?
-        get() = if (BuildConfig.DEBUG) debugClient else nonDebugClient
 
     /**
      * Context to be used
      */
     val context: Context get() = factory.context
 
-    init {
+//    init {
 //        context.sharedPrefs.preferences.registerOnSharedPreferenceChangeListener(this)
-        initialize()
-    }
-
-    /**
-     * Initialize loader
-     */
-    private fun initialize() {
-        debugClient = OkHttpClient.Builder().apply {
-            readTimeout(30, TimeUnit.SECONDS)
-            writeTimeout(30, TimeUnit.SECONDS)
-            connectTimeout(30, TimeUnit.SECONDS)
-            followRedirects(true)
-            followSslRedirects(true)
-            addInterceptor(
-                HttpLoggingInterceptor().setLevel(
-                    HttpLoggingInterceptor.Level.HEADERS
-                )
-            )
-//            addInterceptor(factory.chuckInterceptor)
-        }.build()
-    }
+//    }
 
     /**
      * De-initialize loader

+ 17 - 12
tvlib/src/main/kotlin/org/mjdev/tvlib/glide/sources/albumart/AlbumArtLoaderFactory.kt

@@ -25,23 +25,22 @@ import okhttp3.Cache
 import okhttp3.OkHttpClient
 import okhttp3.Request
 import okhttp3.logging.HttpLoggingInterceptor
+import org.mjdev.tvlib.BuildConfig
 import org.mjdev.tvlib.extensions.GlobalExt.launch
 import org.mjdev.tvlib.helpers.http.UserAgentInterceptor
-import org.mjdev.tvlib.network.CacheInterceptor
 import org.mjdev.tvlib.helpers.http.MimeTypeMapUtils
+import org.mjdev.tvlib.helpers.http.NetworkConnectionInterceptor
 import org.xmlpull.v1.XmlPullParser
 import java.io.ByteArrayInputStream
 import java.io.File
 import java.io.InputStream
 
 // todo better performance && precache && dagger
-@Suppress("UNCHECKED_CAST", "PrivatePropertyName")
+@Suppress("UNCHECKED_CAST")
 class AlbumArtLoaderFactory(
     private val context: Context
 ) : ModelLoader<String?, InputStream?>, ModelLoaderFactory<String?, InputStream?> {
 
-    private val USER_AGENT =
-        "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0"
     private val httpCache by lazy {
         Cache(
             directory = File(
@@ -51,18 +50,24 @@ class AlbumArtLoaderFactory(
             maxSize = 1024L * 1024L * 1024L
         )
     }
-    private val userAgentInterceptor by lazy { UserAgentInterceptor(USER_AGENT) }
-    private val cacheInterceptor by lazy { CacheInterceptor() }
+    private val userAgentInterceptor by lazy { UserAgentInterceptor() }
+    private val networkConnectionInterceptor by lazy { NetworkConnectionInterceptor(context) }
+
+    //    private val adBlockInterceptor by lazy { AdBlockInterceptor(context) }
+//    private val cacheInterceptor by lazy { CacheInterceptor() }
     private val httpLoggingInterceptor by lazy {
-        HttpLoggingInterceptor().apply {
-            level = HttpLoggingInterceptor.Level.BODY
-        }
+        HttpLoggingInterceptor().setLevel(
+            if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
+            else HttpLoggingInterceptor.Level.NONE
+        )
     }
     private val httpClient: OkHttpClient by lazy {
         OkHttpClient.Builder().apply {
+            addNetworkInterceptor(networkConnectionInterceptor)
             addNetworkInterceptor(userAgentInterceptor)
-            addNetworkInterceptor(cacheInterceptor)
             addNetworkInterceptor(httpLoggingInterceptor)
+//            addNetworkInterceptor(cacheInterceptor)
+//            addNetworkInterceptor(adBlockInterceptor)
             cache(httpCache)
         }.build()
     }
@@ -86,7 +91,7 @@ class AlbumArtLoaderFactory(
             try {
                 releaseGroupID = parse(musicSearchIs)
             } catch (e: Exception) {
-               // no op
+                // no op
             }
         }
         if (releaseGroupID != null) {
@@ -220,7 +225,7 @@ class AlbumArtLoaderFactory(
                     } else {
                         callback.onLoadFailed(Exception("No cover"))
                     }
-                } catch (e:Exception) {
+                } catch (e: Exception) {
                     callback.onLoadFailed(e)
                 }
             }

+ 78 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/AccessTokenInterceptor.kt

@@ -0,0 +1,78 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.helpers.http
+
+import android.text.TextUtils
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+import java.io.IOException
+import java.net.HttpURLConnection
+
+@Suppress("unused")
+class AccessTokenInterceptor internal constructor(
+    private val sessionPreferenceManager: SessionPreferenceManager,
+    private val sessionRepository: ISessionRepository
+) : Interceptor {
+
+    private val token: String
+        get() = sessionPreferenceManager.token
+
+    @Throws(IOException::class)
+    override fun intercept(chain: Interceptor.Chain): Response {
+        var request = chain.request()
+        val builder = request.newBuilder()
+        val token = sessionPreferenceManager.token
+        addAuthHeader(builder)
+        request = builder.build()
+        val response = chain.proceed(request)
+        if (response.code == HttpURLConnection.HTTP_UNAUTHORIZED) {
+            synchronized(this) {
+                val newToken = sessionPreferenceManager.token
+                return if (token != newToken) {
+                    chain.proceed(newRequestWithAccessToken(request, newToken))
+                } else try {
+                    refreshToken()
+                    addAuthHeader(builder)
+                    request = builder.build()
+                    return chain.proceed(request)
+                } catch (error: AuthenticationError) {
+                    logout()
+                    response
+                } catch (error: ServerException) {
+                    response
+                }
+            }
+        }
+        return response
+    }
+
+    private fun newRequestWithAccessToken(
+        request: Request, accessToken: String
+    ): Request {
+        return request.newBuilder()
+            .header("Authorization", "Bearer $accessToken")
+            .build()
+    }
+
+    private fun addAuthHeader(builder: Request.Builder) {
+        if (!TextUtils.isEmpty(token)) {
+            builder.addHeader("Authorization", "Bearer $token")
+        }
+    }
+
+    @Throws(AuthenticationError::class, ServerException::class)
+    private fun refreshToken() {
+        return sessionRepository.refreshToken()
+    }
+
+    private fun logout() {
+        sessionRepository.logout()
+    }
+}

+ 11 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/AuthenticationError.kt

@@ -0,0 +1,11 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.helpers.http
+
+class AuthenticationError : Exception("Authentication error.")

+ 4 - 18
tvlib/src/main/kotlin/org/mjdev/tvlib/network/CacheInterceptor.kt → tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/CacheInterceptor.kt

@@ -1,12 +1,12 @@
 /*
- * Copyright (c) Milan Jurkulák 2023.
+ *  Copyright (c) Milan Jurkulák 2023.
  *  Contact:
  *  e: mimoccc@gmail.com
  *  e: mj@mjdev.org
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvlib.network
+package org.mjdev.tvlib.helpers.http
 
 import okhttp3.CacheControl
 import okhttp3.Interceptor
@@ -14,26 +14,12 @@ import okhttp3.Response
 import java.io.IOException
 import java.util.concurrent.TimeUnit
 
-/**
- * Cache interceptor.
- * Made to cache result from internet.
- * Basically for an one hour, to avoid multiple calls.
- *
- * @constructor Create [CacheInterceptor]
- * @property maxAge
- * @property timeUnit
- */
+@Suppress("unused")
 class CacheInterceptor(
     private val maxAge: Int = 1,
     private val timeUnit: TimeUnit = TimeUnit.HOURS
 ) : Interceptor {
 
-    /**
-     * Intercept network calls.
-     *
-     * @param chain Chain
-     * @return [Response]
-     */
     @Throws(IOException::class)
     override fun intercept(chain: Interceptor.Chain): Response {
         val response: Response = chain.proceed(chain.request())
@@ -47,4 +33,4 @@ class CacheInterceptor(
             .build()
     }
 
-}
+}

+ 53 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/ErrorInterceptor.kt

@@ -0,0 +1,53 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.helpers.http
+
+import okhttp3.Interceptor
+import okhttp3.Response
+import timber.log.Timber
+
+@Suppress("unused")
+class ErrorInterceptor(
+    val fromJson: (body: String, cls: Class<BaseResponse>) -> BaseResponse?
+) : Interceptor {
+    override fun intercept(chain: Interceptor.Chain): Response {
+        val response = chain.proceed(chain.request())
+        if (!response.isSuccessful) {
+            when (response.code) {
+                in 401 until 403 -> throw AuthenticationError()
+                in 500 until 599 -> throw ServerException()
+                else -> {
+                }
+            }
+        } else {
+            val genericResponse: BaseResponse = parseError(response) ?: return response
+            if (!genericResponse.status) {
+                Timber.e("Generic Message thrown with message: ${genericResponse.message}")
+                throw GenericMessageException(genericResponse.message)
+            }
+        }
+        return response
+    }
+
+    private fun parseError(response: Response): BaseResponse? {
+        return try {
+            val responseBodyCopy = response.peekBody(Long.MAX_VALUE)
+            val bodyString = responseBodyCopy.string()
+            fromJson(bodyString, BaseResponse::class.java)
+        } catch (e: Exception) {
+            return null
+        }
+    }
+
+    interface BaseResponse {
+        val status: Boolean
+        val message: String
+    }
+
+}

+ 11 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/GenericMessageException.kt

@@ -0,0 +1,11 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.helpers.http
+
+class GenericMessageException(message: String) : Exception(message)

+ 21 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/ISessionRepository.kt

@@ -0,0 +1,21 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.helpers.http
+
+@Suppress("unused")
+class ISessionRepository {
+    fun logout() {
+
+    }
+
+    fun refreshToken() {
+
+    }
+
+}

+ 43 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/NetworkConnectionInterceptor.kt

@@ -0,0 +1,43 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.helpers.http
+
+import android.content.Context
+import android.net.ConnectivityManager
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+import java.io.IOException
+
+@Suppress("unused")
+class NetworkConnectionInterceptor(
+    private val context: Context
+) : Interceptor {
+
+    @Throws(IOException::class)
+    override fun intercept(chain: Interceptor.Chain): Response {
+        if (!isConnected()) {
+            throw NoConnectivityException()
+        }
+        val builder: Request.Builder = chain.request().newBuilder()
+        return chain.proceed(builder.build())
+    }
+
+    @Suppress("DEPRECATION")
+    private fun isConnected(): Boolean {
+        val cm = context.getSystemService(
+            Context.CONNECTIVITY_SERVICE
+        ) as ConnectivityManager
+        val activeNetwork = cm.activeNetworkInfo
+        return activeNetwork != null && activeNetwork.isConnected
+    }
+
+    class NoConnectivityException : Exception("No internet connection.")
+
+}

+ 11 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/ServerException.kt

@@ -0,0 +1,11 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.helpers.http
+
+class ServerException : Exception("Server error.")

+ 14 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/SessionPreferenceManager.kt

@@ -0,0 +1,14 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.helpers.http
+
+@Suppress("unused")
+class SessionPreferenceManager {
+    var token: String = ""
+}

+ 5 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/UserAgentInterceptor.kt

@@ -12,7 +12,11 @@ import okhttp3.Request
 import okhttp3.Response
 import java.io.IOException
 
-class UserAgentInterceptor(private val userAgent: String) : Interceptor {
+@Suppress("unused")
+class UserAgentInterceptor(
+    private val userAgent: String =
+        "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0"
+) : Interceptor {
     @Throws(IOException::class)
     override fun intercept(chain: Interceptor.Chain): Response {
         val originalRequest: Request = chain.request()

+ 13 - 14
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/audiopreview/dailymotion/DailyMotionVideoView.kt

@@ -27,8 +27,9 @@ import okhttp3.Cache
 import okhttp3.OkHttpClient
 import okhttp3.Request
 import okhttp3.logging.HttpLoggingInterceptor
+import org.mjdev.tvlib.BuildConfig
+import org.mjdev.tvlib.helpers.http.NetworkConnectionInterceptor
 import org.mjdev.tvlib.helpers.http.UserAgentInterceptor
-import org.mjdev.tvlib.network.CacheInterceptor
 import org.mjdev.tvlib.ui.components.audiopreview.IPreviewEngine
 import timber.log.Timber
 import java.io.File
@@ -62,9 +63,6 @@ class DailyMotionVideoView @JvmOverloads constructor(
     private val DM_URL =
         "https://api.dailymotion.com/videos?fields=id,thumbnail_url,title&search=%s&page=1&limit=1"
 
-    private val USER_AGENT =
-        "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0"
-
     private val httpCache by lazy {
         Cache(
             directory = File(
@@ -75,24 +73,25 @@ class DailyMotionVideoView @JvmOverloads constructor(
         )
     }
 
-    private val userAgentInterceptor by lazy {
-        UserAgentInterceptor(USER_AGENT)
-    }
+    private val networkConnectionInterceptor by lazy { NetworkConnectionInterceptor(context) }
+    private val userAgentInterceptor by lazy { UserAgentInterceptor() }
 
-    private val cacheInterceptor by lazy {
-        CacheInterceptor()
-    }
+//    private val cacheInterceptor by lazy {
+//        CacheInterceptor()
+//    }
 
     private val httpLoggingInterceptor by lazy {
-        HttpLoggingInterceptor().apply {
-            level = HttpLoggingInterceptor.Level.BODY
-        }
+        HttpLoggingInterceptor().setLevel(
+            if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
+            else HttpLoggingInterceptor.Level.NONE
+        )
     }
 
     private val httpClient: OkHttpClient by lazy {
         OkHttpClient.Builder().apply {
+            addNetworkInterceptor(networkConnectionInterceptor)
             addNetworkInterceptor(userAgentInterceptor)
-            addNetworkInterceptor(cacheInterceptor)
+//            addNetworkInterceptor(cacheInterceptor)
             addNetworkInterceptor(httpLoggingInterceptor)
             cache(httpCache)
         }.build()

+ 0 - 28
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/SettingsPage.kt

@@ -1,28 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-package org.mjdev.tvlib.ui.components.page
-
-import android.annotation.SuppressLint
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Settings
-import androidx.compose.runtime.Composable
-import org.mjdev.tvlib.R
-import org.mjdev.tvlib.annotations.Previews
-
-open class SettingsPage : Page() {
-
-    override val title: Any? = R.string.menu_title_settings
-    override val icon: Any? = Icons.Default.Settings
-
-    @SuppressLint("ComposableNaming")
-    @Previews
-    @Composable
-    override fun Content() = super.Content()
-
-}

+ 29 - 12
tvlib/src/main/kotlin/org/mjdev/tvlib/updater/AppUpdater.kt

@@ -31,7 +31,8 @@ import okhttp3.OkHttpClient
 import okhttp3.logging.HttpLoggingInterceptor
 import org.mjdev.tvlib.BuildConfig
 import org.mjdev.tvlib.data.github.remote.Release
-import org.mjdev.tvlib.network.CacheInterceptor
+import org.mjdev.tvlib.helpers.http.NetworkConnectionInterceptor
+import org.mjdev.tvlib.helpers.http.UserAgentInterceptor
 import retrofit2.Retrofit
 import retrofit2.converter.moshi.MoshiConverterFactory
 import java.text.SimpleDateFormat
@@ -51,7 +52,12 @@ class AppUpdater(
     }
 
     private val releases: Flow<List<Release>> = flow {
-        updateService(isDebug, githubUser, githubRepository).also { service ->
+        updateService(
+            context,
+            githubUser,
+            githubRepository,
+            isDebug,
+        ).also { service ->
             emit(service.releases().getOrNull() ?: emptyList())
         }
     }
@@ -109,26 +115,36 @@ class AppUpdater(
 
     companion object {
 
-        private fun httpLoggingInterceptor() = HttpLoggingInterceptor().apply {
-            level = HttpLoggingInterceptor.Level.BODY
-        }
-
         private fun okHttpClient(
             isDebug: Boolean = false,
-            cacheInterceptor: CacheInterceptor = CacheInterceptor(),
+            networkConnectionInterceptor: NetworkConnectionInterceptor,
+            userAgentInterceptor: UserAgentInterceptor,
+//            cacheInterceptor: CacheInterceptor = CacheInterceptor(),
 //            adBlockInterceptor: AdBlockInterceptor = AdBlockInterceptor(),
-            httpLoggingInterceptor: HttpLoggingInterceptor = httpLoggingInterceptor(),
+            httpLoggingInterceptor: HttpLoggingInterceptor = HttpLoggingInterceptor().setLevel(
+                if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
+                else HttpLoggingInterceptor.Level.NONE
+            ),
         ): OkHttpClient = OkHttpClient.Builder().apply {
-            addNetworkInterceptor(cacheInterceptor)
+            addNetworkInterceptor(networkConnectionInterceptor)
+            addNetworkInterceptor(userAgentInterceptor)
+//            addNetworkInterceptor(cacheInterceptor)
 //            addNetworkInterceptor(adBlockInterceptor)
+
             if (isDebug) {
                 addInterceptor(httpLoggingInterceptor)
             }
         }.build()
 
         private fun retrofit(
-            okHttpClient: OkHttpClient,
+            context: Context,
             baseUrl: String = "",
+            isDebug: Boolean = false,
+            okHttpClient: OkHttpClient = okHttpClient(
+                isDebug = isDebug,
+                userAgentInterceptor = UserAgentInterceptor(),
+                networkConnectionInterceptor = NetworkConnectionInterceptor(context)
+            ),
         ): Retrofit = Retrofit.Builder()
             .baseUrl(baseUrl)
             .client(okHttpClient)
@@ -138,11 +154,12 @@ class AppUpdater(
             .build()
 
         fun updateService(
-            isDebug: Boolean = false,
+            context: Context,
             githubUser: String = "mimoccc",
             githubRepository: String = "tvapp",
+            isDebug: Boolean = false,
             baseUrl: String = "https://api.github.com/repos/$githubUser/$githubRepository/",
-            retrofit: Retrofit = retrofit(okHttpClient(isDebug = isDebug), baseUrl)
+            retrofit: Retrofit = retrofit(context, baseUrl, isDebug)
         ): AppUpdateService = retrofit.create(AppUpdateService::class.java)
 
         @Composable

+ 16 - 29
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/WebVideoScrapper.kt

@@ -11,8 +11,6 @@ package org.mjdev.tvlib.webscrapper
 import android.content.Context
 import androidx.core.net.toUri
 import com.gargoylesoftware.htmlunit.BrowserVersion
-import org.mjdev.tvlib.webscrapper.fetcher.BlockingFetcher
-import org.mjdev.tvlib.webscrapper.base.Request
 import org.mjdev.tvlib.webscrapper.base.response
 import org.mjdev.tvlib.webscrapper.base.skrape
 import org.jsoup.nodes.Document
@@ -29,12 +27,9 @@ class WebVideoScrapper(
     private val onVideoFound: (video: Video) -> Unit = {},
     private val onFinish: (videos: List<Video>) -> Unit = {}
 ) {
-    private val fetcher: BlockingFetcher<Request> by lazy {
-        BrowserFetcher(context, addBlock)
-    }
-    private val links: ArrayDeque<String> = ArrayDeque<String>().apply {
-        addAll(baseUrls)
-    }
+    private val fetcher by lazy { BrowserFetcher(context, addBlock) }
+    private val links = ArrayDeque<String>().apply { addAll(baseUrls) }
+    private val scannedLinks = mutableListOf<String>()
     private val videos = mutableListOf<Video>()
 
     fun start() {
@@ -49,6 +44,7 @@ class WebVideoScrapper(
     }
 
     private fun parse(uri: String) {
+        scannedLinks.add(uri)
         try {
             skrape(fetcher) {
                 request {
@@ -61,25 +57,21 @@ class WebVideoScrapper(
                 response {
                     htmlDocument {
                         eachVideo.forEach { src ->
-                            val url = normalizedUrl(baseUri, src)
-                            val title = parseTitle(document, url)
-                            val category = parseCategory(document, url)
-                            val thumb = parseThumb(document, url)
                             val video = Video(
-                                title = title,
-                                category = category,
-                                url = url,
-                                thumb = thumb
+                                title = parseTitle(document, src),
+                                category = parseCategory(document, src),
+                                url = src,
+                                thumb = parseThumb(document, src)
                             )
                             videos.add(video)
                             onVideoFound(video)
                         }
                         eachLink.values.forEach { href ->
-                            if ((!href.startsWith("#")) && (href != "/") && (!href.contentEquals(
-                                    baseUri
-                                ))
-                            ) {
-                                links.add(normalizedUrl(baseUri, href))
+                            when {
+                                href.startsWith("javascript:") -> {}
+                                href.contentEquals(baseUri) -> {}
+                                scannedLinks.contains(href) -> {}
+                                else -> links.add(href)
                             }
                         }
                     }
@@ -106,13 +98,7 @@ class WebVideoScrapper(
     @Suppress("UNUSED_PARAMETER")
     private fun parseTitle(document: Document, url: String): String {
         val auth = document.location().toUri().authority.toString()
-        return document.title().replace(auth, "", ignoreCase = true)
-    }
-
-    private fun normalizedUrl(base: String, href: String): String = href.let {
-        if (it.startsWith("/")) it.replaceFirstChar { "" } else it
-    }.let {
-        if (base.contentEquals(it)) base else "$base/$it"
+        return document.title().replace(auth, "", ignoreCase = true).trim()
     }
 
     data class Video(
@@ -121,4 +107,5 @@ class WebVideoScrapper(
         val category: String,
         val thumb: String
     )
-}
+
+}

+ 8 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/AdBlock.kt

@@ -22,7 +22,7 @@ import java.net.URL
 @Suppress("unused")
 class AdBlock(
     inputStream: InputStream,
-    private val blockMimeTypes : Boolean = false
+    private val blockMimeTypes: Boolean = false
 ) : IAdBlocker {
 
     constructor(file: File) : this(file.inputStream())
@@ -74,9 +74,13 @@ class AdBlock(
             null
         }
         Timber.d("host: $host")
-        val mimeType = MimeTypeMapUtils.getMimeTypeFromUrl(reqUrl)
-        Timber.d("mimetype: $mimeType")
-        if (blockMimeTypes && mimeType?.let { allowedMimeTypes.contains(mimeType) } == false) return true
+        if (blockMimeTypes) {
+            val mimeType = MimeTypeMapUtils.getMimeTypeFromUrl(reqUrl)
+            Timber.d("mimetype: $mimeType")
+            if (mimeType?.let { allowedMimeTypes.contains(it) } == false) {
+                return true
+            }
+        }
         if (host?.let { h -> hosts.contains(h) } == true) return true
         return reqUrl?.let { url ->
             urls.any { blockedURL -> url.startsWith(blockedURL) }

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/AdBlockInterceptor.kt

@@ -28,11 +28,11 @@ class AdBlockInterceptor(
     override fun intercept(chain: Interceptor.Chain): Response {
         val request: Request = chain.request()
         val reqUrl = request.url
-        val originResponse: Response = chain.proceed(request)
         return if (adBlock.isBlocked(reqUrl.toUrl()))
             emptyResponse(request)
         else {
             val cache = request.header(CacheType.KEY_CACHE)
+            val originResponse: Response = chain.proceed(request)
             if (!TextUtils.isEmpty(cache) && (cache == CacheType.NORMAL.ordinal.toString() + ""))
                 originResponse
             else originResponse.newBuilder()

+ 30 - 24
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/fetcher/BrowserFetcher.kt

@@ -16,51 +16,56 @@ import com.gargoylesoftware.htmlunit.util.Cookie
 import com.gargoylesoftware.htmlunit.util.NameValuePair
 import org.mjdev.tvlib.webscrapper.base.toCookie
 import org.mjdev.tvlib.webscrapper.base.urlOrigin
-import okhttp3.Cache
 import okhttp3.OkHttpClient
 import okhttp3.logging.HttpLoggingInterceptor
+import org.mjdev.tvlib.BuildConfig
+import org.mjdev.tvlib.helpers.http.NetworkConnectionInterceptor
 import org.mjdev.tvlib.webscrapper.adblock.IAdBlocker
 import org.mjdev.tvlib.webscrapper.adblock.NoAdBlock
 import org.mjdev.tvlib.helpers.http.UserAgentInterceptor
-import org.mjdev.tvlib.network.CacheInterceptor
+import org.mjdev.tvlib.webscrapper.adblock.AdBlockInterceptor
 import org.mjdev.tvlib.webscrapper.base.Method
 import org.mjdev.tvlib.webscrapper.base.Request
 import org.mjdev.tvlib.webscrapper.base.Result
-import java.io.File
 import java.net.Proxy
 import java.net.URL
 
 @Suppress("unused")
 class BrowserFetcher(
     private val context: Context,
-    private val adBlock: IAdBlocker = NoAdBlock(),
-    private val userAgent: String = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0"
+    private val adBlock: IAdBlocker = NoAdBlock()
 ) : BlockingFetcher<Request> {
-    private val httpCache by lazy {
-        Cache(
-            directory = File(
-                context.cacheDir,
-                "http_cache"
-            ),
-            maxSize = 1024L * 1024L * 1024L
-        )
-    }
-    private val userAgentInterceptor by lazy { UserAgentInterceptor(userAgent) }
-    private val cacheInterceptor by lazy { CacheInterceptor() }
+    //    private val httpCache by lazy {
+//        Cache(
+//            directory = File(
+//                context.cacheDir,
+//                "http_cache"
+//            ),
+//            maxSize = 1024L * 1024L * 1024L
+//        )
+//    }
+    private val userAgentInterceptor by lazy { UserAgentInterceptor() }
+    private val networkConnectionInterceptor by lazy { NetworkConnectionInterceptor(context) }
+
+    //    private val cacheInterceptor by lazy { CacheInterceptor() }
+    private val adBlockInterceptor by lazy { AdBlockInterceptor(context, adBlock) }
     private val httpLoggingInterceptor by lazy {
-        HttpLoggingInterceptor().apply {
-            level = HttpLoggingInterceptor.Level.BODY
-        }
+        HttpLoggingInterceptor().setLevel(
+            if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
+            else HttpLoggingInterceptor.Level.NONE
+        )
     }
 
-    //    private val adBlockInterceptor by lazy { AdBlockInterceptor(context, adBlock) }
     private val httpClient by lazy {
         OkHttpClient.Builder().apply {
-//            addNetworkInterceptor(adBlockInterceptor)
+            addNetworkInterceptor(networkConnectionInterceptor)
             addNetworkInterceptor(userAgentInterceptor)
-            addNetworkInterceptor(cacheInterceptor)
-            addNetworkInterceptor(httpLoggingInterceptor)
-            cache(httpCache)
+            if (BuildConfig.DEBUG) {
+                addNetworkInterceptor(httpLoggingInterceptor)
+            }
+            addInterceptor(adBlockInterceptor)
+//            addNetworkInterceptor(cacheInterceptor)
+//            cache(httpCache)
         }.build()
     }
 
@@ -129,6 +134,7 @@ class BrowserFetcher(
             historyPageCacheLimit = 0
             isJavaScriptEnabled = true
             withProxySettings(request)
+            waitForBackgroundJavaScript(1000)
         }
     }
 

+ 4 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/DocElement.kt

@@ -32,6 +32,10 @@ class DocElement internal constructor(
 
     val attributeValues: List<String> by lazy { attributes.map { it.value } }
 
+    val baseUri: String by lazy { element.baseUri() }
+
+    fun absUrl(attributeKey: String): String = element.absUrl(attributeKey)
+
     infix fun attribute(attributeKey: String): String = attributes[attributeKey].orEmpty()
 
     fun hasAttribute(attributeKey: String): Boolean = attribute(attributeKey).isNotBlank()

+ 5 - 5
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/DomTreeElement.kt

@@ -57,7 +57,7 @@ abstract class DomTreeElement : CssSelectable() {
         get(): Map<String, String> = allElements.filter {
             it.hasAttribute("href")
         }.associate {
-            it.text to it.attribute("href")
+            it.text to it.absUrl("href")
         }
 
     @Suppress("unused")
@@ -65,15 +65,15 @@ abstract class DomTreeElement : CssSelectable() {
         get(): Map<String, String> =
             allElements.filter { it.tagName == "img" }
                 .filter { it.hasAttribute("src") }
-                .associate { it.attribute("alt") to it.attribute("src") }
+                .associate { it.attribute("alt") to it.absUrl("src") }
 
     val eachVideo: List<String>
         get() {
             val elements = allElements
-            val videos = elements.filter { e -> (e.tagName == "video") || (e.tagName == "source") }
+            val videos = elements.filter { e -> e.tagName == "source" }
+            // todo mimetype
             val videosWithSource = videos.filter { e -> e.hasAttribute("src") }
-            // todo type
-            return videosWithSource.map { e -> e.attribute("src") }
+            return videosWithSource.map { e -> e.absUrl("src") }
         }
 
     open fun makeDefaultElement(cssSelector: String): DocElement {

+ 12 - 0
wiki/Release-Notes-1.2.2.md

@@ -0,0 +1,12 @@
+* Release Notes 1.2.2
+* config field fix / proguard fix
+* build improvements
+* scrapper base
+* navigation improvements
+* navigation improvements / need more improvements
+* code improvements
+* code improvements
+* scroll improvements
+* update fix
+* update fix
+* update fix