Przeglądaj źródła

scrapper base

scrapper base
Milan Jurkulak 2 lat temu
rodzic
commit
5510ddd4bc
85 zmienionych plików z 2612 dodań i 2429 usunięć
  1. 3 1
      app/build.gradle.kts
  2. 1 3
      app/src/main/AndroidManifest.xml
  3. 30 1
      app/src/main/kotlin/org/mjdev/tvapp/database/DAO.kt
  4. 6 0
      app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt
  5. 108 124
      app/src/main/kotlin/org/mjdev/tvapp/sync/SyncAdapter.kt
  6. 4 18
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/DetailViewModel.kt
  7. 1 2
      tvlib/build.gradle.kts
  8. 0 87
      tvlib/src/main/kotlin/org/mjdev/tvlib/adblock/AdBlock.kt
  9. 0 41
      tvlib/src/main/kotlin/org/mjdev/tvlib/adblock/HttpCacheInterceptor.kt
  10. 8 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt
  11. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/glide/sources/albumart/AlbumArtLoaderFactory.kt
  12. 19 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/CacheType.kt
  13. 10 6
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/MimeTypeMapUtils.kt
  14. 2 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/updater/AppUpdater.kt
  15. 0 178
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/CustomWebChromeClient.kt
  16. 0 136
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/CustomWebViewClient.kt
  17. 0 63
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/WebClient.kt
  18. 0 75
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/WebScrapper.kt
  19. 0 162
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/AssetsLoader.kt
  20. 0 93
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/CacheExtensionConfig.kt
  21. 0 6
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/CacheType.kt
  22. 0 54
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/DynamicCacheLoader.kt
  23. 0 34
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/FileUtil.kt
  24. 0 25
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/HttpCacheInterceptor.kt
  25. 0 40
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/MD5Utils.kt
  26. 0 53
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/NetUtils.kt
  27. 0 48
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/OKHttpFile.kt
  28. 0 5
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/ResourceInterceptor.kt
  29. 0 62
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/TimeUtils.kt
  30. 0 380
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/WebViewCacheInterceptor.kt
  31. 0 86
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/WebViewCacheInterceptorInst.kt
  32. 0 33
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/WebViewRequestInterceptor.kt
  33. 0 25
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cookies/CookiesManager.kt
  34. 0 18
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlAnchor.kt
  35. 0 8
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlBody.kt
  36. 0 8
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlDiv.kt
  37. 0 25
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlDocument.kt
  38. 0 83
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlElement.kt
  39. 0 15
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlHead.kt
  40. 0 15
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlImage.kt
  41. 0 12
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlLink.kt
  42. 0 12
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlMeta.kt
  43. 0 10
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlNoScript.kt
  44. 0 20
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlPage.kt
  45. 0 13
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlScript.kt
  46. 0 11
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlSource.kt
  47. 0 11
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlTitle.kt
  48. 0 98
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlVideo.kt
  49. 0 91
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/javascript/JSI.kt
  50. 0 7
      tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/javascript/JSIListener.kt
  51. 124 130
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/WebVideoScrapper.kt
  52. 92 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/AdBlock.kt
  53. 53 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/AdBlockInterceptor.kt
  54. 18 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/IAdBlocker.kt
  55. 21 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/NoAdBlock.kt
  56. 42 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/Authentication.kt
  57. 115 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/BodyBuilder.kt
  58. 75 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/Cookie.kt
  59. 26 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/ProxyBuilder.kt
  60. 64 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/Request.kt
  61. 46 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/Result.kt
  62. 98 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/Scraper.kt
  63. 100 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/UrlBuilder.kt
  64. 21 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/fetcher/BaseFetcher.kt
  65. 209 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/fetcher/BrowserFetcher.kt
  66. 102 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/parser/Parser.kt
  67. 89 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/CssSelectable.kt
  68. 65 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/CssSelector.kt
  69. 36 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/Doc.kt
  70. 180 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/DocElement.kt
  71. 106 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/DomTreeElement.kt
  72. 19 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/CustomTagSelectors.kt
  73. 24 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/DemarcatingEditsSelectors.kt
  74. 60 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/EmbeddedContentSelectors.kt
  75. 96 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/FormSelectors.kt
  76. 42 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/InteractiveSelectors.kt
  77. 18 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/MainRootSelectors.kt
  78. 48 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/MediaSelectors.kt
  79. 48 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/MetadataSelectors.kt
  80. 30 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/ScriptingSelectors.kt
  81. 76 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/SectioningSelectors.kt
  82. 52 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/TableSelectors.kt
  83. 64 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/TextContentSelectors.kt
  84. 132 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/TextSemanticsSelectors.kt
  85. 28 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/WebComponentsSelectors.kt

+ 3 - 1
app/build.gradle.kts

@@ -13,9 +13,9 @@ import org.mjdev.gradle.plugin.MainAppPlugin.Companion.kotlinCompilerExtVersion
 import org.mjdev.gradle.plugin.MainAppPlugin.Companion.loadKeyStoreProperties
 import org.mjdev.gradle.plugin.MainAppPlugin.Companion.projectCompileSdk
 import org.mjdev.gradle.plugin.MainAppPlugin.Companion.projectMinSdk
+import org.mjdev.gradle.plugin.MainAppPlugin.Companion.suffixToString
 import org.mjdev.gradle.plugin.MainAppPlugin.Companion.versionCode
 import org.mjdev.gradle.plugin.MainAppPlugin.Companion.versionName
-import org.mjdev.gradle.plugin.MainAppPlugin.Companion.suffixToString
 
 @Suppress("PropertyName")
 val CONFIG_KEYSTORE_PROPERTIES_FILE = "config/keystore.properties"
@@ -270,6 +270,8 @@ dependencies {
     implementation("androidx.media3:media3-decoder:1.2.0")
     implementation("androidx.media3:media3-datasource:1.2.0")
     implementation("androidx.media3:media3-common:1.2.0")
+    // generative a.i.
+//    implementation("com.google.ai.client.generativeai:generativeai:0.1.1")
     // libs mismatch
     constraints {
         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.0").apply {

+ 1 - 3
app/src/main/AndroidManifest.xml

@@ -182,8 +182,7 @@
 
         <service
             android:name=".sync.SyncService"
-            android:exported="true"
-            android:process=":sync"
+            android:exported="false"
             tools:ignore="ExportedService">
 
             <intent-filter>
@@ -215,7 +214,6 @@
             android:authorities="@string/sync_auth"
             android:enabled="true"
             android:exported="true"
-            android:process=":provider"
             android:syncable="true"
             tools:ignore="ExportedContentProvider" />
 

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

@@ -1,4 +1,10 @@
-@file:Suppress("unused")
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
 
 package org.mjdev.tvapp.database
 
@@ -15,6 +21,7 @@ import org.mjdev.tvapp.data.local.Message
 import org.mjdev.tvapp.data.local.Movie
 import org.mjdev.tvapp.data.local.MyObjectBox
 import org.mjdev.tvapp.data.local.TVChannel
+import kotlin.reflect.KClass
 
 class DAO(
     @ApplicationContext
@@ -41,6 +48,11 @@ class DAO(
             it.name == name
         }?.call(this)
 
+        inline fun <T> with(receiver: T, block: T.() -> Unit): T {
+            block.invoke(receiver)
+            return receiver
+        }
+
     }
 
     private val store: BoxStore by lazy {
@@ -50,14 +62,18 @@ class DAO(
             .build()
     }
 
+    val cache = mutableMapOf<KClass<*>, List<*>>()
+
     val movieDao: Box<Movie> by lazy {
         store.boxFor()
     }
 
+    @Suppress("unused")
     val categoryDao: Box<Category> by lazy {
         store.boxFor()
     }
 
+    @Suppress("unused")
     val channelsDao: Box<TVChannel> by lazy {
         store.boxFor()
     }
@@ -70,4 +86,17 @@ class DAO(
         store.boxFor()
     }
 
+    val allMediaItems: List<Any> by lazy {
+        movieDao.all
+    }
+
+    inline fun <reified T> getCachedList(creator: () -> List<*>): List<*> {
+        if (!cache.containsKey(T::class)) {
+            creator().let { list ->
+                cache[T::class] = list
+            }
+        }
+        return cache[T::class] ?: emptyList<Any>()
+    }
+
 }

+ 6 - 0
app/src/main/kotlin/org/mjdev/tvapp/module/ProvideModule.kt

@@ -85,12 +85,18 @@ class ProvideModule {
     @Provides
     fun providesCacheInterceptor() = CacheInterceptor()
 
+//    @Singleton
+//    @Provides
+//    fun provideAdBlockInterceptor() = AdBlockInterceptor()
+
     @Singleton
     @Provides
     fun providesOkHttpClient(
         cacheInterceptor: CacheInterceptor,
+//        adBlockInterceptor: AdBlockInterceptor,
         httpLoggingInterceptor: HttpLoggingInterceptor
     ): OkHttpClient = OkHttpClient.Builder().apply {
+//        addNetworkInterceptor(adBlockInterceptor)
         addNetworkInterceptor(cacheInterceptor)
         if (BuildConfig.DEBUG) {
             addInterceptor(httpLoggingInterceptor)

+ 108 - 124
app/src/main/kotlin/org/mjdev/tvapp/sync/SyncAdapter.kt

@@ -1,3 +1,11 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
 package org.mjdev.tvapp.sync
 
 import android.accounts.Account
@@ -11,11 +19,13 @@ import io.objectbox.Box
 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.tvlib.extensions.GlobalExt.IO
-import org.mjdev.tvlib.extensions.GlobalExt.launch
+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
@@ -29,11 +39,10 @@ class SyncAdapter(
     private val movieDao: Box<Movie> get() = dao.movieDao
 
     private val urls = listOf(
+        "https://scartissue.site/",
         "https://prehraj.to/hledej/filmy",
-//        "https://meatyhunks.com",
     )
 
-
     override fun onPerformSync(
         account: Account?,
         extras: Bundle?,
@@ -41,140 +50,115 @@ class SyncAdapter(
         provider: ContentProviderClient?,
         syncResult: SyncResult?
     ) {
-        launch(IO) {
-            try {
-                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
-                            })
-                        }
+        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)
+            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)
+                }
+                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)
+                }
+                toRemove.forEach { movie ->
+                    try {
+                        dao.movieDao.tx {
+                            remove(movie)
                         }
+                        Timber.d("Movie: $movie removed.")
+                    } catch (e: Exception) {
+                        Timber.e(e)
                     }
                 }
+            }
 
-                // todo non ui thread
-//            launch(UI) {
-//                WebScrapper(context, urls) { page, videoURL ->
-//                    try {
-//                        val auth = page?.url?.toUri()?.authority ?: ""
-//                        val movie = Movie().apply {
-//                            this.category = auth
-//                            this.title = page?.title?.replace(auth, "")
-//                            this.image = videoURL
-//                            this.uri = videoURL
-//                        }
-//                        dao.movieDao.put(movie)
-//                        Timber.d("Movie: $movie stored.")
-//                    } catch (e: Exception) {
-//                        Timber.e(e)
-//                    }
-//                }.start()
-//            }
-
-//            WebVideoScrapper(
-//                baseUrls = urls,
-//                onVideoFound = { video ->
-//                    dao.movieDao.put(Movie().apply {
-//                        category = video.category
-//                        title = video.title
-//                        image = video.thumb
-//                        uri = video.url
-//                    })
-//                },
-//                onFinish = { videos ->
-//
-//                }
-//            ).start()
+            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()
 
-            } catch (e: Exception) {
-                Timber.e(e)
-            }
         }
     }
-
-    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)
-            }
-
+}
+
+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)
         }
-
     }
-
 }

+ 4 - 18
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/DetailViewModel.kt

@@ -23,7 +23,6 @@ import org.mjdev.tvlib.interfaces.ItemAudio
 import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemVideo
 import javax.inject.Inject
-import kotlin.reflect.KClass
 
 @Suppress("MemberVisibilityCanBePrivate")
 @HiltViewModel
@@ -41,29 +40,16 @@ class DetailViewModel @Inject constructor() : BaseViewModel() {
     @Inject
     lateinit var localPhotoCursor: PhotoCursor
 
-    val mediaItems: List<Any> get() = dao.movieDao.all
-
-    val cache = mutableMapOf<KClass<*>, List<*>>()
-
     fun mediaItemsFor(
         data: Any?
     ): List<Any?> = when (data) {
-        is ItemAudio -> getCachedList<ItemAudio> { localAudioCursor }
-        is ItemVideo -> getCachedList<ItemVideo> { localVideoCursor }
-        is ItemPhoto -> getCachedList<ItemPhoto> { localPhotoCursor }
-        is Movie -> getCachedList<Movie> { mediaItems }
+        is ItemAudio -> dao.getCachedList<ItemAudio> { localAudioCursor }
+        is ItemVideo -> dao.getCachedList<ItemVideo> { localVideoCursor }
+        is ItemPhoto -> dao.getCachedList<ItemPhoto> { localPhotoCursor }
+        is Movie -> dao.getCachedList<Movie> { dao.allMediaItems }
         else -> listOf(data.mediaItem)
     }
 
-    private inline fun <reified T> getCachedList(creator: () -> List<*>): List<*> {
-        if (!cache.containsKey(T::class)) {
-            creator().let { list ->
-                cache[T::class] = list
-            }
-        }
-        return cache[T::class] ?: emptyList<Any>()
-    }
-
     companion object {
 
         @Suppress("unused")

+ 1 - 2
tvlib/build.gradle.kts

@@ -202,10 +202,9 @@ dependencies {
 //    implementation("com.stripe:stripecardscan:20.33.0")
 //    implementation("com.stripe:financial-connections:20.33.0")
     // scrape
-    implementation("it.skrape:skrapeit:1.2.2")
-    implementation("it.skrape:skrapeit-browser-fetcher:1.2.2")
 //    implementation("org.jsoup:jsoup:1.16.1")
 //    implementation("net.sourceforge.htmlunit:htmlunit-android:2.67.0")
+    implementation("net.sourceforge.htmlunit:htmlunit-android:2.67.0")
     // yt
     // ...
     // constraints

+ 0 - 87
tvlib/src/main/kotlin/org/mjdev/tvlib/adblock/AdBlock.kt

@@ -1,87 +0,0 @@
-/*
- *  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.adblock
-
-import android.net.Uri
-import org.mjdev.tvlib.webclient.cache.MimeTypeMapUtils
-import org.mjdev.tvlib.extensions.StringExt.removeComments
-import org.mjdev.tvlib.extensions.StringExt.removeEmptyLines
-import org.mjdev.tvlib.extensions.StringExt.trimLines
-import timber.log.Timber
-import java.io.File
-import java.io.InputStream
-import java.net.URL
-
-class AdBlock(inputStream: InputStream) {
-    private var hosts = mutableListOf<String>()
-    private val urls = mutableListOf<String>()
-
-    // todo may be another
-    private val allowedMimeTypes = mutableListOf(
-        "text/html",
-        "text/plain",
-        "text/javascript",
-        "application/javascript",
-        "video/mp4",
-        "application/json",
-    )
-
-    init {
-        inputStream
-            .bufferedReader()
-            .readLines()
-            .removeEmptyLines()
-            .trimLines()
-            .removeComments('#')
-            .map { line ->
-                when (line[0]) {
-                    // host starts with '*'
-                    '*' -> hosts.add(line.substring(1))
-                    // url to block
-                    else -> urls.add(line)
-                }
-            }
-    }
-
-    @Synchronized
-    fun isBlocked(reqUrl: String?): Boolean {
-        val host = try {
-            Uri.parse(reqUrl).host
-        } catch (e: Exception) {
-            Timber.e(e)
-            null
-        }
-        Timber.d("host: $host")
-        val mimeType = MimeTypeMapUtils.getMimeTypeFromUrl(reqUrl)
-        Timber.d("mimetype: $mimeType")
-        if (mimeType?.let { allowedMimeTypes.contains(mimeType) } == false) return true
-        if (host?.let { h -> hosts.contains(h) } == true) return true
-        return reqUrl?.let { url ->
-            urls.any { blockedURL -> url.startsWith(blockedURL) }
-        } ?: false
-    }
-
-    fun isBlocked(reqUrl: URL?): Boolean = isBlocked(reqUrl.toString())
-
-    fun isBlocked(reqUrl: Uri?): Boolean = isBlocked(reqUrl.toString())
-
-    companion object {
-
-        @Suppress("MemberVisibilityCanBePrivate")
-        fun adBlock(
-            fileWithData: File = File("file:///android_asset/adblock.txt")
-        ) = AdBlock(fileWithData.inputStream())
-
-        val instance by lazy { adBlock() }
-
-    }
-
-}

+ 0 - 41
tvlib/src/main/kotlin/org/mjdev/tvlib/adblock/HttpCacheInterceptor.kt

@@ -1,41 +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.adblock
-
-import android.text.TextUtils
-import okhttp3.Interceptor
-import okhttp3.Request
-import okhttp3.Response
-import org.mjdev.tvlib.webclient.cache.CacheType
-import org.mjdev.tvlib.webclient.cache.WebViewCacheInterceptor
-import java.io.IOException
-
-class AdBlockInterceptor : Interceptor {
-
-    @Throws(IOException::class)
-    override fun intercept(chain: Interceptor.Chain): Response {
-        val request: Request = chain.request()
-        val reqUrl = request.url
-        val adBlock = AdBlock.instance
-        return if (adBlock.isBlocked(reqUrl.toUrl())) {
-            Response.Builder().code(404).build()
-        } else {
-            val cache = request.header(WebViewCacheInterceptor.KEY_CACHE)
-            val originResponse: Response = chain.proceed(request)
-            if (!TextUtils.isEmpty(cache) && cache == CacheType.NORMAL.ordinal.toString() + "")
-                originResponse
-            else originResponse.newBuilder()
-                .removeHeader("pragma")
-                .removeHeader("Cache-Control")
-                .header("Cache-Control", "max-age=3153600000")
-                .build()
-        }
-    }
-
-}

+ 8 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt

@@ -44,6 +44,14 @@ object GlobalExt {
         block = block
     )
 
+    fun launchUI (
+        block: suspend CoroutineScope.() -> Unit
+    ) = launch(UI) { block() }
+
+    fun launchIO (
+        block: suspend CoroutineScope.() -> Unit
+    ) = launch(IO) { block() }
+
     fun <T> T.postDelayed(
         delay: Long,
         block: T.() -> Unit

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

@@ -28,7 +28,7 @@ import okhttp3.logging.HttpLoggingInterceptor
 import org.mjdev.tvlib.extensions.GlobalExt.launch
 import org.mjdev.tvlib.helpers.http.UserAgentInterceptor
 import org.mjdev.tvlib.network.CacheInterceptor
-import org.mjdev.tvlib.webclient.cache.MimeTypeMapUtils
+import org.mjdev.tvlib.helpers.http.MimeTypeMapUtils
 import org.xmlpull.v1.XmlPullParser
 import java.io.ByteArrayInputStream
 import java.io.File

+ 19 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/CacheType.kt

@@ -0,0 +1,19 @@
+/*
+ *  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")
+enum class CacheType {
+    NORMAL,
+    FORCE;
+
+    companion object {
+        const val KEY_CACHE = "Cache"
+    }
+}

+ 10 - 6
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/MimeTypeMapUtils.kt → tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/http/MimeTypeMapUtils.kt

@@ -1,9 +1,17 @@
-package org.mjdev.tvlib.webclient.cache
+/*
+ *  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.webkit.MimeTypeMap
 import java.util.Locale
 
-@Suppress("Deprecation")
+@Suppress("Deprecation", "MemberVisibilityCanBePrivate")
 object MimeTypeMapUtils {
 
     fun getFileExtensionFromUrl(url: String?): String {
@@ -27,8 +35,4 @@ object MimeTypeMapUtils {
         return MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileExtensionFromUrl(url))
     }
 
-//    fun getMimeTypeFromExtension(extension: String?): String? {
-//        return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
-//    }
-
 }

+ 2 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/updater/AppUpdater.kt

@@ -116,9 +116,11 @@ class AppUpdater(
         private fun okHttpClient(
             isDebug: Boolean = false,
             cacheInterceptor: CacheInterceptor = CacheInterceptor(),
+//            adBlockInterceptor: AdBlockInterceptor = AdBlockInterceptor(),
             httpLoggingInterceptor: HttpLoggingInterceptor = httpLoggingInterceptor(),
         ): OkHttpClient = OkHttpClient.Builder().apply {
             addNetworkInterceptor(cacheInterceptor)
+//            addNetworkInterceptor(adBlockInterceptor)
             if (isDebug) {
                 addInterceptor(httpLoggingInterceptor)
             }

+ 0 - 178
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/CustomWebChromeClient.kt

@@ -1,178 +0,0 @@
-package org.mjdev.tvlib.webclient
-
-import android.annotation.SuppressLint
-import android.graphics.Bitmap
-import android.net.Uri
-import android.os.Message
-import android.view.View
-import android.webkit.*
-import timber.log.Timber
-
-@SuppressLint("SetJavaScriptEnabled")
-@Suppress("Deprecation")
-class CustomWebChromeClient(
-    private val webView: WebClient
-) : WebChromeClient() {
-
-    private val settings: WebSettings get() = webView.settings
-
-    init {
-        settings.apply {
-            userAgentString = WebClient.USER_AGENT
-            javaScriptEnabled = true
-            javaScriptCanOpenWindowsAutomatically = false
-            mediaPlaybackRequiresUserGesture = false
-            allowContentAccess = false
-            allowFileAccess = false
-            allowFileAccessFromFileURLs = false
-            allowUniversalAccessFromFileURLs = false
-            databaseEnabled = false
-            displayZoomControls = false
-            domStorageEnabled = false
-//            layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
-//            loadWithOverviewMode = true
-            loadsImagesAutomatically = false
-            mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
-//            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-//                offscreenPreRaster = true
-//            }
-//            useWideViewPort = true
-//            setAppCacheEnabled(true)
-            setSupportMultipleWindows(false)
-            setSupportZoom(false)
-            setGeolocationEnabled(false)
-            pluginState = WebSettings.PluginState.OFF
-//            setRenderPriority(WebSettings.RenderPriority.HIGH)
-        }
-    }
-
-    override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
-        Timber.e("Got console message: ${consoleMessage?.message()}")
-        return super.onConsoleMessage(consoleMessage)
-    }
-
-    override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
-    }
-
-    override fun onHideCustomView() {
-    }
-
-    override fun onProgressChanged(view: WebView?, newProgress: Int) {
-    }
-
-    override fun onCloseWindow(window: WebView?) {
-    }
-
-    override fun onCreateWindow(
-        view: WebView?,
-        isDialog: Boolean,
-        isUserGesture: Boolean,
-        resultMsg: Message?
-    ): Boolean {
-        return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg)
-    }
-
-    override fun onGeolocationPermissionsHidePrompt() {
-    }
-
-    override fun onGeolocationPermissionsShowPrompt(
-        origin: String?,
-        callback: GeolocationPermissions.Callback?
-    ) {
-    }
-
-    override fun onJsAlert(
-        view: WebView?,
-        url: String?,
-        message: String?,
-        result: JsResult?
-    ): Boolean {
-        return super.onJsAlert(view, url, message, result)
-    }
-
-    override fun onJsBeforeUnload(
-        view: WebView?,
-        url: String?,
-        message: String?,
-        result: JsResult?
-    ): Boolean {
-        return super.onJsBeforeUnload(view, url, message, result)
-    }
-
-    override fun onJsConfirm(
-        view: WebView?,
-        url: String?,
-        message: String?,
-        result: JsResult?
-    ): Boolean {
-        return super.onJsConfirm(view, url, message, result)
-    }
-
-    override fun onJsPrompt(
-        view: WebView?,
-        url: String?,
-        message: String?,
-        defaultValue: String?,
-        result: JsPromptResult?
-    ): Boolean {
-        return super.onJsPrompt(view, url, message, defaultValue, result)
-    }
-
-    override fun onPermissionRequest(request: PermissionRequest?) {
-    }
-
-    override fun onPermissionRequestCanceled(request: PermissionRequest?) {
-    }
-
-    override fun onReceivedIcon(view: WebView?, icon: Bitmap?) {
-    }
-
-    override fun onReceivedTitle(view: WebView?, title: String?) {
-    }
-
-    override fun onReceivedTouchIconUrl(
-        view: WebView?,
-        url: String?,
-        precomposed: Boolean
-    ) {
-    }
-
-    override fun onRequestFocus(view: WebView?) {
-    }
-
-    override fun onShowFileChooser(
-        view: WebView?,
-        filePathCallback: ValueCallback<Array<Uri>>?,
-        fileChooserParams: FileChooserParams?
-    ): Boolean {
-        return super.onShowFileChooser(webView, filePathCallback, fileChooserParams)
-    }
-
-    @Deprecated("Deprecated in Java", ReplaceWith(
-        "Timber.e(\"\$sourceID \$lineNumber : \$message\")",
-        "timber.log.Timber"
-    )
-    )
-    override fun onConsoleMessage(message: String?, lineNumber: Int, sourceID: String?) {
-        Timber.e("$sourceID $lineNumber : $message")
-    }
-
-    @Deprecated("Deprecated in Java")
-    override fun onExceededDatabaseQuota(
-        url: String?,
-        databaseIdentifier: String?,
-        quota: Long,
-        estimatedDatabaseSize: Long,
-        totalQuota: Long,
-        quotaUpdater: WebStorage.QuotaUpdater?
-    ) {
-    }
-
-    @Deprecated("Deprecated in Java")
-    override fun onShowCustomView(
-        view: View?,
-        requestedOrientation: Int,
-        callback: CustomViewCallback?
-    ) {
-    }
-}

+ 0 - 136
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/CustomWebViewClient.kt

@@ -1,136 +0,0 @@
-package org.mjdev.tvlib.webclient
-
-import android.annotation.SuppressLint
-import android.graphics.Bitmap
-import android.net.http.SslError
-import android.os.Message
-import android.view.KeyEvent
-import android.webkit.*
-import org.mjdev.tvlib.webclient.html.HtmlPage
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.mjdev.tvlib.webclient.cache.WebViewCacheInterceptorInst
-import timber.log.Timber
-
-open class CustomWebViewClient(
-    private val webView: WebClient,
-    private val onGotHtml: (htmlPage: HtmlPage, loadedResources: List<String>) -> Unit,
-) : WebViewClient() {
-    val loadedResources = mutableListOf<String>()
-    val jsi: JSI by lazy { JSI(webView, onGotHtml) }
-
-    @Synchronized
-    override fun shouldInterceptRequest(
-        view: WebView?,
-        request: WebResourceRequest?
-    ): WebResourceResponse? {
-        val uri = request?.url?.toString()
-        uri?.let {
-            if (!loadedResources.contains(it)) {
-                loadedResources.add(it)
-            }
-        }
-        Timber.d("Loading url: $uri")
-        return WebViewCacheInterceptorInst.instance?.interceptRequest(request)
-    }
-
-    @Synchronized
-    override fun shouldOverrideUrlLoading(
-        view: WebView?,
-        request: WebResourceRequest?
-    ): Boolean {
-        return super.shouldOverrideUrlLoading(view, request)
-    }
-
-    override fun onLoadResource(view: WebView?, url: String?) {
-        url?.let {
-            if (!loadedResources.contains(it)) {
-                loadedResources.add(it)
-            }
-        }
-    }
-
-    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
-    }
-
-    @SuppressLint("NewApi")
-    override fun onReceivedError(
-        view: WebView?,
-        request: WebResourceRequest?,
-        error: WebResourceError?
-    ) {
-        Timber.e("Error received: (${error?.errorCode}) ${error?.description}")
-    }
-
-    override fun onReceivedHttpAuthRequest(
-        view: WebView?,
-        handler: HttpAuthHandler?,
-        host: String?,
-        realm: String?
-    ) {
-        super.onReceivedHttpAuthRequest(view, handler, host, realm)
-    }
-
-    override fun onReceivedHttpError(
-        view: WebView?,
-        request: WebResourceRequest?,
-        error: WebResourceResponse?
-    ) {
-        Timber.e("Error received: (${error?.statusCode}) ${error?.reasonPhrase}")
-    }
-
-    override fun onReceivedLoginRequest(
-        view: WebView?,
-        realm: String?,
-        account: String?,
-        args: String?
-    ) {
-        // todo login screen
-        Timber.e("Login request. Unsupported yet.")
-    }
-
-    override fun onReceivedSslError(
-        view: WebView?,
-        handler: SslErrorHandler?,
-        error: SslError?
-    ) {
-        super.onReceivedSslError(view, handler, error)
-        Timber.e("SSL error: %s", error.toString())
-    }
-
-    override fun onScaleChanged(view: WebView?, oldScale: Float, newScale: Float) {
-    }
-
-    override fun onUnhandledKeyEvent(view: WebView?, event: KeyEvent?) {
-        super.onUnhandledKeyEvent(view, event)
-    }
-
-    @Deprecated("Deprecated in Java")
-    override fun onReceivedError(
-        view: WebView?,
-        errorCode: Int,
-        description: String?,
-        failingUrl: String?
-    ) {
-        Timber.e("Error at $failingUrl:")
-        Timber.e("$errorCode : $description")
-    }
-
-    @Deprecated(
-        "Deprecated in Java",
-        ReplaceWith("Timber.e(\"Too many redirects.\")", "timber.log.Timber")
-    )
-    override fun onTooManyRedirects(
-        view: WebView?,
-        cancelMsg: Message?,
-        continueMsg: Message?
-    ) {
-        Timber.e("Too many redirects.")
-    }
-
-    override fun onPageFinished(view: WebView?, url: String?) {
-        if (!url.contentEquals("about:blank")) {
-            jsi.getHtml()
-        }
-    }
-
-}

+ 0 - 63
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/WebClient.kt

@@ -1,63 +0,0 @@
-package org.mjdev.tvlib.webclient
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.view.ViewGroup
-import android.webkit.*
-import org.mjdev.tvlib.webclient.html.HtmlPage
-import org.mjdev.tvlib.webclient.javascript.JSI
-
-@SuppressLint("ViewConstructor")
-@Suppress("unused", "MemberVisibilityCanBePrivate")
-class WebClient constructor(
-    context: Context,
-    val block: (page: HtmlPage?, resources: List<String>) -> Unit
-) : WebView(context) {
-    val webChromeClient: CustomWebChromeClient by lazy {
-        CustomWebChromeClient(this)
-    }
-    val webViewClient: CustomWebViewClient by lazy {
-        CustomWebViewClient(this) { page, res ->
-            onGotHtml(page, res)
-        }
-    }
-    val js: JSI get() = webViewClient.jsi
-
-    init {
-        setWebChromeClient(webChromeClient)
-        setWebViewClient(webViewClient)
-    }
-
-    fun onGotHtml(htmlPage: HtmlPage, loadedResources: List<String>) {
-        if (htmlPage.url != "about:blank") {
-            block(htmlPage, loadedResources)
-        }
-    }
-
-    fun getPage(url: String) {
-        loadUrl(url)
-    }
-
-    fun close() {
-        loadUrl("about:blank")
-    }
-
-    fun attachViewTo(
-        viewGroup: ViewGroup?,
-        layoutParams: ViewGroup.LayoutParams
-    ): WebClient {
-        detachView()
-        viewGroup?.addView(this, layoutParams)
-        return this
-    }
-
-    fun detachView() {
-        (parent as ViewGroup?)?.removeView(this)
-    }
-
-    companion object {
-        const val USER_AGENT =
-            "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0"
-    }
-
-}

+ 0 - 75
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/WebScrapper.kt

@@ -1,75 +0,0 @@
-package org.mjdev.tvlib.webclient
-
-import android.content.Context
-import org.mjdev.tvlib.webclient.html.HtmlAnchor
-import org.mjdev.tvlib.webclient.html.HtmlPage
-import org.mjdev.tvlib.webclient.html.HtmlVideo
-import java.util.*
-import kotlin.collections.ArrayDeque
-
-@Suppress("unused")
-class WebScrapper(
-    context: Context,
-    urls: List<String> = listOf(),
-    private val block: (page: HtmlPage?, videoURL: String?) -> Unit
-) {
-
-    @Suppress("UNUSED_ANONYMOUS_PARAMETER")
-    private val webClient by lazy {
-        WebClient(context) { page: HtmlPage?, resources: List<String>? ->
-            if (page != null) {
-                parsePage(page, page.videos, page.anchors)
-            }
-        }
-    }
-
-    private val parsePages: ArrayDeque<String> = ArrayDeque<String>().apply {
-        addAll(urls)
-    }
-
-    @Throws(IllegalStateException::class)
-    fun start() {
-        if (parsePages.size > 0) {
-            parseNextPage()
-        } else {
-            throw (IllegalStateException("No url to parse"))
-        }
-    }
-
-    private fun parseNextPage() {
-        if (parsePages.size > 0) {
-            parsePageFromUrl(parsePages.removeFirst())
-        }
-    }
-
-    private fun parsePageFromUrl(pageUrl: String?) {
-        if (pageUrl != null) {
-            webClient.getPage(pageUrl)
-        }
-    }
-
-    private fun parsePage(page: HtmlPage, videos: List<HtmlVideo>, anchors: List<HtmlAnchor>) {
-        anchors.map { anchor ->
-            anchor.fullHref(page)
-        }.forEach { href ->
-            if (href.isNotEmpty()) {
-                parsePages.add(href)
-            }
-        }
-        parseVideos(page, videos)
-        parseNextPage()
-    }
-
-    private fun parseVideos(page: HtmlPage, videos: List<HtmlVideo>) {
-        if (videos.isNotEmpty()) {
-            videos.forEach { video ->
-                video.videoStreams.forEach { streamURL ->
-                    if (streamURL.isNotEmpty()) {
-                        block.invoke(page, streamURL)
-                    }
-                }
-            }
-        }
-    }
-
-}

+ 0 - 162
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/AssetsLoader.kt

@@ -1,162 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import android.content.Context
-import android.text.TextUtils
-import timber.log.Timber
-import java.io.File
-import java.io.IOException
-import java.io.InputStream
-import java.net.MalformedURLException
-import java.net.URL
-import java.util.*
-import java.util.concurrent.CopyOnWriteArraySet
-
-class AssetsLoader {
-
-    private var context: Context? = null
-    private var assetResSet: CopyOnWriteArraySet<String>? = null
-    private var dir = ""
-    private var cleared = false
-    private var isSuffixMod = false
-
-    fun isAssetsSuffixMod(suffixMod: Boolean): AssetsLoader {
-        isSuffixMod = suffixMod
-        return this
-    }
-
-    fun init(context: Context?): AssetsLoader {
-        this.context = context
-        assetResSet = CopyOnWriteArraySet()
-        cleared = false
-        return this
-    }
-
-    private fun getUrlPath(url: String): String {
-        var uPath = ""
-        try {
-            val u = URL(url)
-            uPath = u.path
-            if (uPath.startsWith("/")) {
-                if (uPath.length == 1) {
-                    return uPath
-                }
-                uPath = uPath.substring(1)
-            }
-        } catch (e: MalformedURLException) {
-            Timber.e(e)
-        }
-        return uPath
-    }
-
-    fun getResByUrl(url: String): InputStream? {
-        val uPath = getUrlPath(url)
-        if (TextUtils.isEmpty(uPath))
-            return null
-        if (!isSuffixMod) {
-            return if (TextUtils.isEmpty(dir)) {
-                getAssetFileStream(uPath)
-            } else {
-                getAssetFileStream(dir + File.separator + uPath)
-            }
-        }
-        if (assetResSet != null) {
-            for (p in assetResSet!!) {
-                if (uPath.endsWith(p)) {
-                    return if (TextUtils.isEmpty(dir)) {
-                        getAssetFileStream(p)
-                    } else {
-                        getAssetFileStream(dir + File.separator + p)
-                    }
-                }
-            }
-        }
-        return null
-    }
-
-    fun setDir(dir: String): AssetsLoader {
-        this.dir = dir
-        return this
-    }
-
-    fun initData(): AssetsLoader {
-        if (!isSuffixMod)
-            return this
-        if ((assetResSet?.size ?: 0) == 0)
-            Thread {
-                initResourceNoneRecursion(dir)
-            }.start()
-        return this
-    }
-
-    fun clear() {
-        cleared = true
-        assetResSet?.clear()
-    }
-
-    private fun addAssetsFile(f: String) {
-        var file = f
-        val flag = dir + File.separator
-        if (!TextUtils.isEmpty(dir)) {
-            val pos = file.indexOf(flag)
-            if (pos >= 0) {
-                file = file.substring(pos + flag.length)
-            }
-        }
-        assetResSet?.add(file)
-    }
-
-    private fun initResourceNoneRecursion(dir: String): AssetsLoader {
-        try {
-            val list = LinkedList<String>()
-            val resData = context?.assets?.list(dir) ?: emptyArray()
-            for (res in resData) {
-                val sub = dir + File.separator + res
-                val tmp = context?.assets?.list(sub) ?: emptyArray()
-                if (tmp.isEmpty()) {
-                    addAssetsFile(sub)
-                } else {
-                    list.add(sub)
-                }
-            }
-            while (!list.isEmpty()) {
-                if (cleared) break
-                val last = list.removeFirst()
-                val tmp = context?.assets?.list(last) ?: emptyArray()
-                if (tmp.isEmpty()) {
-                    addAssetsFile(last)
-                } else {
-                    for (sub in tmp) {
-                        val tmp1 =
-                            context?.assets?.list(last + File.separator + sub) ?: emptyArray()
-                        if (tmp1.isEmpty()) {
-                            addAssetsFile(last + File.separator + sub)
-                        } else {
-                            list.add(last + File.separator + sub)
-                        }
-                    }
-                }
-            }
-        } catch (e: IOException) {
-            Timber.e(e)
-        }
-        return this
-    }
-
-    private fun getAssetFileStream(path: String): InputStream? {
-        try {
-            return context?.assets?.open(path)
-        } catch (e: IOException) {
-            Timber.e(e)
-        }
-        return null
-    }
-
-    companion object {
-
-        val instance: AssetsLoader by lazy {
-            AssetsLoader()
-        }
-
-    }
-
-}

+ 0 - 93
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/CacheExtensionConfig.kt

@@ -1,93 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import android.text.TextUtils
-import java.util.Locale
-
-class CacheExtensionConfig {
-
-    private val statics: ArrayList<*> = ArrayList<Any?>(STATIC)
-    private val noCache: ArrayList<*> = ArrayList<Any?>(NO_CACHE)
-
-    fun isMedia(extension: String): Boolean {
-        if (TextUtils.isEmpty(extension)) return false
-        return if (NO_CACHE.contains(extension)) true
-        else noCache.contains(extension.lowercase(Locale.getDefault()).trim { it <= ' ' })
-    }
-
-    fun canCache(extension: String): Boolean {
-        var ext = extension
-        if (TextUtils.isEmpty(ext)) return false
-        ext = ext.lowercase(Locale.getDefault()).trim { it <= ' ' }
-        return if (STATIC.contains(ext)) true else statics.contains(extension)
-    }
-
-//    fun addExtension(extension: String): CacheExtensionConfig {
-//        add(statics, extension)
-//        return this
-//    }
-
-//    fun removeExtension(extension: String): CacheExtensionConfig {
-//        remove(statics, extension)
-//        return this
-//    }
-
-    fun isHtml(extension: String): Boolean {
-        if (TextUtils.isEmpty(extension)) {
-            return false
-        }
-        return extension.lowercase(Locale.getDefault()).contains("html") ||
-                extension.lowercase(Locale.getDefault()).contains("htm")
-    }
-
-    @Suppress("unused")
-    fun clearAll() {
-        clearDiskExtension()
-    }
-
-    private fun clearDiskExtension() {
-        statics.clear()
-    }
-
-    companion object {
-
-        private val STATIC = mutableListOf(
-            "html", "htm", "js", "ico",
-            "css", "png", "jpg", "jpeg",
-            "gif", "bmp", "ttf", "woff",
-            "woff2", "otf", "eot", "svg",
-            "xml", "swf", "txt", "text",
-            "conf", "webp",
-        )
-
-        private val NO_CACHE = mutableListOf(
-            "mp4", "mp3", "ogg",
-            "avi", "wmv", "flv",
-            "rmvb", "3gp",
-        )
-
-//        fun addGlobalExtension(extension: String) {
-//            add(STATIC, extension)
-//        }
-
-//        fun removeGlobalExtension(extension: String) {
-//            remove(STATIC, extension)
-//        }
-
-//        private fun add(set: ArrayList<*>, extension: String) {
-//            if (TextUtils.isEmpty(extension)) {
-//                return
-//            }
-//            val ext = extension.replace(".", "")
-//                .lowercase(Locale.getDefault())
-//                .trim { it <= ' ' }
-//            set.add(ext)
-//        }
-
-//        private fun remove(set: ArrayList<*>, extension: String) {
-//            if (TextUtils.isEmpty(extension)) {
-//                return
-//            }
-//            set.remove(extension.replace(".", "").lowercase(Locale.getDefault()).trim { it <= ' ' })
-//        }
-    }
-}

+ 0 - 6
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/CacheType.kt

@@ -1,6 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-enum class CacheType {
-    NORMAL,
-    FORCE
-}

+ 0 - 54
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/DynamicCacheLoader.kt

@@ -1,54 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import timber.log.Timber
-import java.io.File
-import java.net.MalformedURLException
-import java.net.URL
-
-@Suppress("unused")
-class DynamicCacheLoader {
-
-    fun getResByUrl(file: File?, url: String?): File? {
-        val uPath = getUrlPath(url)
-        if (file?.isDirectory == true) {
-            val files = file.listFiles() ?: emptyArray()
-            for (child in files) {
-                if (child.isDirectory) {
-                    val targetFile = getResByUrl(child, url)
-                    if (targetFile != null) {
-                        return targetFile
-                    }
-                } else {
-                    if (uPath.endsWith(child.name))
-                        return child
-                }
-            }
-        } else {
-            if (uPath.endsWith(file?.name.toString()))
-                return file
-        }
-        return null
-    }
-
-    private fun getUrlPath(url: String?): String {
-        var uPath = ""
-        try {
-            val u = URL(url)
-            uPath = u.path
-            if (uPath.startsWith("/")) {
-                if (uPath.length == 1) return uPath
-                uPath = uPath.substring(1)
-            }
-        } catch (e: MalformedURLException) {
-            Timber.e(e)
-        }
-        return uPath
-    }
-
-    companion object {
-        val instance: DynamicCacheLoader by lazy {
-            DynamicCacheLoader()
-        }
-    }
-
-}

+ 0 - 34
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/FileUtil.kt

@@ -1,34 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import android.text.TextUtils
-import java.io.File
-import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
-
-object FileUtil {
-
-    fun deleteDirs(path: String, isDeleteDir: Boolean) {
-        if (TextUtils.isEmpty(path)) return
-        val dir = File(path)
-        if (!dir.exists()) return
-        val files = dir.listFiles() ?: emptyArray()
-        for (file in files) {
-            if (file.isDirectory) deleteDirs(file.absolutePath, isDeleteDir)
-            else file.delete()
-        }
-        if (isDeleteDir) dir.delete()
-    }
-
-    @Throws(IOException::class)
-    fun copy(inputStream: InputStream, out: OutputStream) {
-        val buf = ByteArray(2048)
-        var len: Int
-        while (inputStream.read(buf).also { readCnt -> len = readCnt } != -1) {
-            out.write(buf, 0, len)
-        }
-        inputStream.close()
-        out.close()
-    }
-
-}

+ 0 - 25
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/HttpCacheInterceptor.kt

@@ -1,25 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import android.text.TextUtils
-import okhttp3.Interceptor
-import okhttp3.Request
-import okhttp3.Response
-import java.io.IOException
-
-class HttpCacheInterceptor : Interceptor {
-
-    @Throws(IOException::class)
-    override fun intercept(chain: Interceptor.Chain): Response {
-        val request: Request = chain.request()
-        val cache = request.header(WebViewCacheInterceptor.KEY_CACHE)
-        val originResponse: Response = chain.proceed(request)
-        return if (!TextUtils.isEmpty(cache) && cache == CacheType.NORMAL.ordinal.toString() + "")
-            originResponse
-        else originResponse.newBuilder()
-            .removeHeader("pragma")
-            .removeHeader("Cache-Control")
-            .header("Cache-Control", "max-age=3153600000")
-            .build()
-    }
-
-}

+ 0 - 40
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/MD5Utils.kt

@@ -1,40 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import timber.log.Timber
-import java.security.MessageDigest
-import java.util.Locale
-
-@Suppress("Deprecation", "unused", "SameParameterValue")
-object MD5Utils {
-
-    fun getMD5(message: String): String {
-        return getMD5(message, true)
-    }
-
-    private fun getMD5(message: String, upperCase: Boolean = true): String {
-        var md5str = ""
-        try {
-            val md = MessageDigest.getInstance("MD5")
-            val input = message.toByteArray()
-            val buff = md.digest(input)
-            md5str = bytesToHex(buff, upperCase)
-        } catch (e: Exception) {
-            Timber.e(e)
-        }
-        return md5str
-    }
-
-    private fun bytesToHex(bytes: ByteArray, upperCase: Boolean): String {
-        val md5str = StringBuffer()
-        var digital: Int
-        for (i in bytes.indices) {
-            digital = bytes[i].toInt()
-            if (digital < 0) digital += 256
-            if (digital < 16) md5str.append("0")
-            md5str.append(Integer.toHexString(digital))
-        }
-        return if (upperCase) md5str.toString().toUpperCase(Locale.ROOT)
-        else md5str.toString().toLowerCase(Locale.ROOT)
-    }
-
-}

+ 0 - 53
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/NetUtils.kt

@@ -1,53 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import android.app.Activity
-import android.content.Context
-import android.net.ConnectivityManager
-import android.text.TextUtils
-import timber.log.Timber
-import java.net.URL
-import java.util.*
-
-object NetUtils {
-
-    @Suppress("Deprecation")
-    fun isConnected(context: Context): Boolean {
-        val cm = context.getSystemService(Activity.CONNECTIVITY_SERVICE) as ConnectivityManager
-        return cm.activeNetworkInfo.let { info ->
-            info != null && info.isConnected
-        }
-    }
-
-    fun getOriginUrl(referer: String?): String? {
-        var ou = referer
-        if (TextUtils.isEmpty(ou)) return ""
-        try {
-            val url = URL(ou)
-            val port = url.port
-            ou = url.protocol + "://" + url.host + if (port == -1) "" else ":$port"
-        } catch (e: Exception) {
-            Timber.e(e)
-        }
-        return ou
-    }
-
-    fun multimapToSingle(maps: Map<String, List<String>?>): Map<String, String> {
-        val sb = StringBuilder()
-        val map: MutableMap<String, String> = HashMap()
-        for ((key, values) in maps) {
-            sb.delete(0, sb.length)
-            if (!values.isNullOrEmpty()) {
-                for (v in values) {
-                    sb.append(v)
-                    sb.append(";")
-                }
-            }
-            if (sb.isNotEmpty()) {
-                sb.deleteCharAt(sb.length - 1)
-            }
-            map[key] = sb.toString()
-        }
-        return map
-    }
-
-}

+ 0 - 48
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/OKHttpFile.kt

@@ -1,48 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import android.text.TextUtils
-import okio.ByteString.Companion.encodeUtf8
-import okio.GzipSource
-import okio.buffer
-import okio.source
-import timber.log.Timber
-import java.io.*
-
-object OKHttpFile {
-
-    private const val ENTRY_METADATA = 0
-    private const val ENTRY_BODY = 1
-
-    fun getCacheFile(path: File, url: String?): InputStream? {
-        if (TextUtils.isEmpty(url)) return null
-        val key: String? = url?.encodeUtf8()?.md5()?.hex()
-        val entryFile = File(path.absolutePath, "$key.$ENTRY_METADATA")
-        val bodyFile = File(path.absolutePath, "$key.$ENTRY_BODY")
-        if (entryFile.exists() && bodyFile.exists()) {
-            try {
-                val fr = BufferedReader(FileReader(entryFile), 1024)
-                var line: String
-                var isGzip = false
-                while (fr.readLine().also { line = it } != null) {
-                    if (line.contains("Content-Encoding") &&
-                        line.contains("gzip")
-                    ) {
-                        isGzip = true
-                        break
-                    }
-                }
-                val inputStream: InputStream = FileInputStream(bodyFile)
-                return if (!isGzip) inputStream
-                else {
-                    var source = bodyFile.source()
-                    source = GzipSource(source)
-                    source.buffer().inputStream()
-                }
-            } catch (e: Exception) {
-                Timber.e(e)
-            }
-        }
-        return null
-    }
-
-}

+ 0 - 5
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/ResourceInterceptor.kt

@@ -1,5 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-interface ResourceInterceptor {
-    fun interceptor(url: String?): Boolean
-}

+ 0 - 62
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/TimeUtils.kt

@@ -1,62 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import android.annotation.SuppressLint
-import timber.log.Timber
-import java.text.ParseException
-import java.text.SimpleDateFormat
-import java.util.*
-
-@Suppress("unused")
-object TimeUtils {
-
-    private const val DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"
-
-    fun formatGMT(time: String): Date? {
-        if (time.indexOf("GMT") < 0) {
-            try {
-                val tt = java.lang.Long.valueOf(time)
-                return Date(tt * 1000)
-            } catch (e: Exception) {
-                Timber.e(e)
-            }
-            return null
-        }
-        try {
-            return SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US)
-                .parse(time.trim { it <= ' ' })
-        } catch (e: ParseException) {
-            Timber.e(e)
-        }
-        return null
-    }
-
-    fun compare(d1: Date?, d2: Date?): Boolean {
-        return if (d1 == null || d2 == null) false
-        else d1.time - d2.time > 0
-    }
-
-    @SuppressLint("SimpleDateFormat")
-    fun getStardTime(time: String?): Date? {
-        return time?.let {
-            try {
-                SimpleDateFormat(DATE_FORMAT).parse(it)
-            } catch (e: ParseException) {
-                Timber.e(e)
-                null
-            }
-        }
-    }
-
-    fun getStardTime(time: Long): Date? {
-        try {
-            return Date(time * 1000)
-        } catch (e: Exception) {
-            Timber.e(e)
-        }
-        return null
-    }
-
-    val currentTime: String
-        @SuppressLint("SimpleDateFormat")
-        get() = SimpleDateFormat(DATE_FORMAT).format(Date())
-}

+ 0 - 380
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/WebViewCacheInterceptor.kt

@@ -1,380 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import android.content.Context
-import android.webkit.URLUtil
-import android.webkit.WebResourceRequest
-import android.webkit.WebResourceResponse
-import android.webkit.WebView
-import okhttp3.Cache
-import okhttp3.CacheControl
-import okhttp3.Dns
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.Response
-import org.mjdev.tvlib.adblock.AdBlockInterceptor
-import timber.log.Timber
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileNotFoundException
-import java.io.IOException
-import java.io.InputStream
-import java.util.concurrent.TimeUnit
-import javax.net.ssl.SSLSession
-import javax.net.ssl.SSLSocketFactory
-import javax.net.ssl.X509TrustManager
-
-// todo trust manager
-@Suppress("unused", "MemberVisibilityCanBePrivate")
-class WebViewCacheInterceptor(builder: Builder) : WebViewRequestInterceptor {
-    override val cachePath: File
-    private val dynamicCacheFile: File?
-    private val cacheSize: Long
-    private val connectTimeout: Long
-    private val readTimeout: Long
-    private val cacheExtensionConfig: CacheExtensionConfig
-    private val context: Context
-    private var cacheType: CacheType
-    private var assetsDir: String = ""
-    private var trustAllHostname = false
-    private var sslSocketFactory: SSLSocketFactory? = null
-    private var x509TrustManager: X509TrustManager? = null
-    private var dns: Dns? = null
-    private val resourceInterceptor: ResourceInterceptor?
-    private var isSuffixMod = false
-    private var httpClient: OkHttpClient? = null
-    private var origin: String? = ""
-    private var referer: String? = ""
-    private var userAgent: String? = ""
-
-    init {
-        cacheExtensionConfig = builder.cacheExtensionConfig
-        cachePath = builder.cacheFile
-        dynamicCacheFile = builder.dynamicCacheFile
-        cacheSize = builder.cacheSize
-        cacheType = builder.cacheType
-        connectTimeout = builder.connectTimeout
-        readTimeout = builder.readTimeout
-        context = builder.context
-        assetsDir = builder.assetsDir
-        x509TrustManager = builder.x509TrustManager
-        sslSocketFactory = builder.sslSocketFactory
-        trustAllHostname = builder.trustAllHostname
-        resourceInterceptor = builder.resourceInterceptor
-        isSuffixMod = builder.isSuffixMod
-        dns = builder.dns
-        initHttpClient()
-        if (isEnableAssets) initAssetsLoader()
-    }
-
-    private val isEnableDynamicCache: Boolean
-        get() = dynamicCacheFile != null
-    private val isEnableAssets: Boolean
-        get() = assetsDir.isNotEmpty()
-
-    private fun initAssetsLoader() {
-        AssetsLoader.instance.init(context).setDir(assetsDir).isAssetsSuffixMod(isSuffixMod)
-    }
-
-    // todo use application client
-    private fun initHttpClient() {
-        val cache = Cache(cachePath, cacheSize)
-        val builder = OkHttpClient.Builder()
-            .cache(cache)
-            .connectTimeout(connectTimeout, TimeUnit.SECONDS)
-            .readTimeout(readTimeout, TimeUnit.SECONDS)
-            .addNetworkInterceptor(HttpCacheInterceptor())
-            .addNetworkInterceptor(AdBlockInterceptor())
-//            .addNetworkInterceptor(LogInterceptor())
-        if (trustAllHostname) {
-            builder.hostnameVerifier { _: String?, _: SSLSession? -> true }
-        }
-        if (sslSocketFactory != null && x509TrustManager != null) {
-            builder.sslSocketFactory(sslSocketFactory!!, x509TrustManager!!)
-        }
-        if (dns != null) {
-            builder.dns(dns!!)
-        }
-        httpClient = builder.build()
-    }
-
-    override fun interceptRequest(request: WebResourceRequest?): WebResourceResponse? {
-        val headers = mutableMapOf<String, String?>().apply {
-            request?.requestHeaders?.also { putAll(it) }
-        }
-        return interceptRequest(request?.url?.toString(), headers)
-    }
-
-    private fun buildHeaders(): MutableMap<String, String?> {
-        val headers: MutableMap<String, String?> = HashMap()
-        if (!origin.isNullOrEmpty()) {
-            headers["Origin"] = origin
-        }
-        if (!referer.isNullOrEmpty()) {
-            headers["Referer"] = referer
-        }
-        if (!userAgent.isNullOrEmpty()) {
-            headers["User-Agent"] = userAgent
-        }
-        return headers
-    }
-
-    override fun interceptRequest(url: String?): WebResourceResponse? {
-        return interceptRequest(url, buildHeaders())
-    }
-
-    private fun checkUrl(url: String?): Boolean {
-        if (url.isNullOrEmpty()) return false
-        if (!url.startsWith("http")) return false
-        if (resourceInterceptor != null && !resourceInterceptor.interceptor(url)) return false
-        val extension: String = MimeTypeMapUtils.getFileExtensionFromUrl(url)
-        if (extension.isEmpty()) return false
-        if (cacheExtensionConfig.isMedia(extension)) return false
-        return cacheExtensionConfig.canCache(extension)
-    }
-
-    override fun loadUrl(webView: WebView?, url: String?) {
-        if (!isValidUrl(url)) return
-        webView?.loadUrl(url ?: "")
-        referer = webView?.url
-        origin = NetUtils.getOriginUrl(referer)
-        userAgent = webView?.settings?.userAgentString
-    }
-
-    override fun loadUrl(url: String?, userAgent: String?) {
-        if (!isValidUrl(url)) return
-        referer = url
-        origin = NetUtils.getOriginUrl(referer)
-        this.userAgent = userAgent
-    }
-
-    override fun loadUrl(
-        url: String?,
-        additionalHttpHeaders: Map<String?, String?>?,
-        userAgent: String?
-    ) {
-        if (!isValidUrl(url)) return
-        referer = url
-        origin = NetUtils.getOriginUrl(referer)
-        this.userAgent = userAgent
-    }
-
-    override fun loadUrl(
-        webView: WebView?,
-        url: String?,
-        additionalHttpHeaders: Map<String?, String?>?
-    ) {
-        if (!isValidUrl(url)) return
-        val headers = mutableMapOf<String?, String?>().apply {
-            additionalHttpHeaders?.also { putAll(it) }
-        }
-        webView?.loadUrl(url ?: "", headers)
-        referer = webView?.url
-        origin = NetUtils.getOriginUrl(referer)
-        userAgent = webView?.settings?.userAgentString
-    }
-
-    override fun clearCache() {
-        FileUtil.deleteDirs(cachePath.absolutePath, false)
-        AssetsLoader.instance.clear()
-    }
-
-    override fun enableForce(force: Boolean) {
-        cacheType = if (force) CacheType.FORCE else CacheType.NORMAL
-    }
-
-    override fun getCacheFile(url: String?): InputStream? {
-        return OKHttpFile.getCacheFile(cachePath, url)
-    }
-
-    override fun initAssetsData() {
-        AssetsLoader.instance.initData()
-    }
-
-    private fun addHeader(reqBuilder: Request.Builder, headers: Map<String, String?>?) {
-        if (headers == null) return
-        for ((key, value) in headers) {
-            reqBuilder.addHeader(key, value ?: "")
-        }
-    }
-
-    private fun interceptRequest(
-        url: String?,
-        headers: MutableMap<String, String?>
-    ): WebResourceResponse? {
-        if (cacheType === CacheType.NORMAL) return null
-        if (!checkUrl(url)) return null
-        if (isEnableDynamicCache) {
-            val file: File? = DynamicCacheLoader.instance.getResByUrl(dynamicCacheFile, url)
-            if (file != null) {
-                Timber.d(String.format("from dynamic file: %s", url))
-                val mimeType: String? = MimeTypeMapUtils.getMimeTypeFromUrl(url)
-                var inputStream: InputStream? = null
-                try {
-                    inputStream = FileInputStream(file)
-                } catch (e: FileNotFoundException) {
-                    Timber.e(e)
-                }
-                return WebResourceResponse(mimeType, "", inputStream)
-            }
-        }
-        if (isEnableAssets) {
-            val inputStream = AssetsLoader.instance.getResByUrl(url!!)
-            if (inputStream != null) {
-                Timber.d("from assets: %s", url)
-                val mimeType: String? = MimeTypeMapUtils.getMimeTypeFromUrl(url)
-                return WebResourceResponse(mimeType, "", inputStream)
-            }
-        }
-        try {
-            val reqBuilder: Request.Builder = Request.Builder().url(url ?: "")
-            val extension: String = MimeTypeMapUtils.getFileExtensionFromUrl(url)
-            if (cacheExtensionConfig.isHtml(extension)) headers[KEY_CACHE] =
-                cacheType.ordinal.toString() + ""
-            addHeader(reqBuilder, headers)
-            if (!NetUtils.isConnected(context)) reqBuilder.cacheControl(CacheControl.FORCE_CACHE)
-            val request: Request = reqBuilder.build()
-            val response: Response? = httpClient?.newCall(request)?.execute()
-            val cacheRes = response?.cacheResponse
-            if (cacheRes != null) Timber.d(
-                String.format(
-                    "from cache: %s",
-                    url
-                )
-            ) else Timber.d(String.format("from server: %s", url))
-            val mimeType: String? = MimeTypeMapUtils.getMimeTypeFromUrl(url)
-            val webResourceResponse =
-                WebResourceResponse(mimeType, "", response?.body?.byteStream())
-            if (response?.code == 504 && !NetUtils.isConnected(context)) return null
-            var message = response?.message
-            if (message.isNullOrEmpty()) message = "OK"
-            try {
-                webResourceResponse.setStatusCodeAndReasonPhrase(
-                    response?.code ?: -1,
-                    message
-                )
-            } catch (e: Exception) {
-                return null
-            }
-            webResourceResponse.responseHeaders =
-                NetUtils.multimapToSingle(response?.headers?.toMultimap() ?: emptyMap())
-            return webResourceResponse
-        } catch (e: IOException) {
-            Timber.e(e)
-        }
-        return null
-    }
-
-    class Builder(val context: Context) {
-        var cacheFile: File
-        var dynamicCacheFile: File? = null
-        var cacheSize = (100 * 1024 * 1024).toLong()
-        var connectTimeout: Long = 20
-        var readTimeout: Long = 20
-        var cacheExtensionConfig: CacheExtensionConfig
-        var debug = true
-        var cacheType = CacheType.FORCE
-        var trustAllHostname = false
-        var sslSocketFactory: SSLSocketFactory? = null
-        var x509TrustManager: X509TrustManager? = null
-        var resourceInterceptor: ResourceInterceptor? = null
-            private set
-        var assetsDir: String = ""
-        var isSuffixMod = false
-        var dns: Dns? = null
-            private set
-
-        init {
-            cacheFile = File(context.cacheDir.toString(), "CacheWebViewCache")
-            cacheExtensionConfig = CacheExtensionConfig()
-        }
-
-        fun setResourceInterceptor(resourceInterceptor: ResourceInterceptor?) {
-            this.resourceInterceptor = resourceInterceptor
-        }
-
-        fun setTrustAllHostname(): Builder {
-            trustAllHostname = true
-            return this
-        }
-
-        fun setSSLSocketFactory(
-            sslSocketFactory: SSLSocketFactory?,
-            trustManager: X509TrustManager?
-        ): Builder {
-            if (sslSocketFactory != null && trustManager != null) {
-                this.sslSocketFactory = sslSocketFactory
-                x509TrustManager = trustManager
-            }
-            return this
-        }
-
-        fun setCachePath(file: File?): Builder {
-            if (file != null) cacheFile = file
-            return this
-        }
-
-        fun setDynamicCachePath(file: File?): Builder {
-            if (file != null) dynamicCacheFile = file
-            return this
-        }
-
-        fun setCacheSize(cacheSize: Long): Builder {
-            if (cacheSize > 1024) this.cacheSize = cacheSize
-            return this
-        }
-
-        fun setReadTimeoutSecond(time: Long): Builder {
-            if (time >= 0) readTimeout = time
-            return this
-        }
-
-        fun setConnectTimeoutSecond(time: Long): Builder {
-            if (time >= 0) connectTimeout = time
-            return this
-        }
-
-        fun setCacheExtensionConfig(config: CacheExtensionConfig?): Builder {
-            if (config != null) cacheExtensionConfig = config
-            return this
-        }
-
-        fun setDebug(debug: Boolean): Builder {
-            this.debug = debug
-            return this
-        }
-
-        fun setCacheType(cacheType: CacheType): Builder {
-            this.cacheType = cacheType
-            return this
-        }
-
-        fun isAssetsSuffixMod(suffixMod: Boolean): Builder {
-            isSuffixMod = suffixMod
-            return this
-        }
-
-        fun setAssetsDir(dir: String?): Builder {
-            if (dir != null) {
-                assetsDir = dir
-            }
-            return this
-        }
-
-        fun setDns(dns: Dns?) {
-            this.dns = dns
-        }
-
-        fun build(): WebViewRequestInterceptor {
-            return WebViewCacheInterceptor(this)
-        }
-    }
-
-    private fun isValidUrl(url: String?): Boolean {
-        return URLUtil.isValidUrl(url)
-    }
-
-    companion object {
-        const val KEY_CACHE = "WebResourceInterceptor-Key-Cache"
-    }
-
-}

+ 0 - 86
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/WebViewCacheInterceptorInst.kt

@@ -1,86 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import android.webkit.WebResourceRequest
-import android.webkit.WebResourceResponse
-import android.webkit.WebView
-import java.io.File
-import java.io.InputStream
-
-@Suppress("unused")
-class WebViewCacheInterceptorInst : WebViewRequestInterceptor {
-
-    private var interceptor: WebViewRequestInterceptor? = null
-
-    fun init(builder: WebViewCacheInterceptor.Builder?) {
-        if (builder != null) interceptor = builder.build()
-    }
-
-    override fun interceptRequest(request: WebResourceRequest?): WebResourceResponse? {
-        return interceptor?.interceptRequest(request)
-    }
-
-    override fun interceptRequest(url: String?): WebResourceResponse? {
-        return interceptor?.interceptRequest(url)
-    }
-
-    override fun loadUrl(webView: WebView?, url: String?) {
-        interceptor?.loadUrl(webView, url)
-    }
-
-    override fun loadUrl(url: String?, userAgent: String?) {
-        interceptor?.loadUrl(url, userAgent)
-    }
-
-    override fun loadUrl(
-        url: String?,
-        additionalHttpHeaders: Map<String?, String?>?,
-        userAgent: String?
-    ) {
-        interceptor?.loadUrl(url, additionalHttpHeaders, userAgent)
-    }
-
-    override fun loadUrl(
-        webView: WebView?,
-        url: String?,
-        additionalHttpHeaders: Map<String?, String?>?
-    ) {
-        interceptor?.loadUrl(webView, url, additionalHttpHeaders)
-    }
-
-    override fun clearCache() {
-        interceptor?.clearCache()
-    }
-
-    override fun enableForce(force: Boolean) {
-        interceptor?.enableForce(force)
-    }
-
-    override fun getCacheFile(url: String?): InputStream? {
-        return interceptor?.getCacheFile(url)
-    }
-
-    override fun initAssetsData() {
-        AssetsLoader.instance.initData()
-    }
-
-    override val cachePath: File?
-        get() = interceptor?.cachePath
-
-    companion object {
-
-        private var webViewCacheInterceptorInst: WebViewCacheInterceptorInst? = null
-
-        val instance: WebViewCacheInterceptorInst?
-            get() {
-                if (webViewCacheInterceptorInst == null) {
-                    synchronized(WebViewCacheInterceptorInst::class.java) {
-                        if (webViewCacheInterceptorInst == null) {
-                            webViewCacheInterceptorInst = WebViewCacheInterceptorInst()
-                        }
-                    }
-                }
-                return webViewCacheInterceptorInst
-            }
-    }
-
-}

+ 0 - 33
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cache/WebViewRequestInterceptor.kt

@@ -1,33 +0,0 @@
-package org.mjdev.tvlib.webclient.cache
-
-import android.webkit.WebResourceRequest
-import android.webkit.WebResourceResponse
-import android.webkit.WebView
-import java.io.File
-import java.io.InputStream
-
-interface WebViewRequestInterceptor {
-
-    val cachePath: File?
-
-    fun interceptRequest(request: WebResourceRequest?): WebResourceResponse?
-
-    fun interceptRequest(url: String?): WebResourceResponse?
-
-    fun clearCache()
-
-    fun enableForce(force: Boolean)
-
-    fun getCacheFile(url: String?): InputStream?
-
-    fun initAssetsData()
-
-    fun loadUrl(webView: WebView?, url: String?)
-
-    fun loadUrl(url: String?, userAgent: String?)
-
-    fun loadUrl(url: String?, additionalHttpHeaders: Map<String?, String?>?, userAgent: String?)
-
-    fun loadUrl(webView: WebView?, url: String?, additionalHttpHeaders: Map<String?, String?>?)
-
-}

+ 0 - 25
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/cookies/CookiesManager.kt

@@ -1,25 +0,0 @@
-package org.mjdev.tvlib.webclient.cookies
-
-import okhttp3.Cookie
-import okhttp3.CookieJar
-import okhttp3.HttpUrl
-
-@Suppress("unused")
-class CookiesManager : HashMap<String, ArrayList<Cookie>>(), CookieJar {
-
-    override fun loadForRequest(url: HttpUrl): List<Cookie> {
-        return url.toString().let { u ->
-            this[u] ?: emptyList()
-        }
-    }
-
-    override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
-        url.toString().let { u ->
-            if (!containsKey(u)) {
-                put(u, ArrayList())
-            }
-            this[u]?.addAll(cookies)
-        }
-    }
-
-}

+ 0 - 18
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlAnchor.kt

@@ -1,18 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-import org.mjdev.tvlib.extensions.GlobalExt.isUrl
-
-class HtmlAnchor(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "a"
-
-    val href get() = attr("href")
-
-    fun fullHref(page: HtmlPage): String {
-        val href = this.href.let { link ->
-            if (link.first() == '/') link.replaceFirst("/", "") else link
-        }
-        return if (href.isUrl) href else "${page.url}$href"
-    }
-}

+ 0 - 8
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlBody.kt

@@ -1,8 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-
-class HtmlBody(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "body"
-}

+ 0 - 8
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlDiv.kt

@@ -1,8 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-
-class HtmlDiv(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "div"
-}

+ 0 - 25
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlDocument.kt

@@ -1,25 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.Jsoup
-import org.jsoup.nodes.Element
-import timber.log.Timber
-import java.lang.Exception
-
-class HtmlDocument(val html: String?, jsi: JSI) : HtmlElement(parseElement(html), jsi) {
-    override val tag: String get() = "html"
-
-    val head: HtmlHead get() = getElement()
-    val body: HtmlBody get() = getElement()
-
-    companion object {
-        fun parseElement(html: String?): Element {
-            return try {
-                Jsoup.parse(html ?: "<html />")
-            } catch (e: Exception) {
-                Timber.e(e)
-                Element("")
-            }
-        }
-    }
-}

+ 0 - 83
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlElement.kt

@@ -1,83 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-
-@Suppress("unused", "MemberVisibilityCanBePrivate", "SpellCheckingInspection")
-abstract class HtmlElement(var element: Element, val jsi: JSI) {
-    abstract val tag: String
-
-    val cssClass: String get() = attr("class")
-    val id: String get() = attr("href")
-    val name get() = attr("name")
-    val childElementCount: Int get() = childrenSize()
-    val childs: List<HtmlElement>
-        get() = element.children().map { e -> parseElement(e, jsi) }
-    val firstChild: HtmlElement
-        get() = parseElement(element.child(0), jsi)
-
-    fun childrenSize(): Int = element.childrenSize()
-
-    fun getElementsByTagName(tagName: String): List<HtmlElement> {
-        return element.getElementsByTag(tagName).map {
-            parseElement(it, jsi)
-        }
-    }
-
-    fun ownText(): String = element.ownText()
-
-    inline fun <reified T> newInstance(jsi: JSI): T = T::class
-        .java
-        .constructors
-        .first()
-        .newInstance(
-            Element("unknown"),
-            jsi
-        ) as T
-
-    inline fun <reified T : HtmlElement> getAllElements(): List<T> {
-        val instance: T = newInstance(jsi)
-        return element.getElementsByTag(instance.tag).mapNotNull {
-            parseElement(it, jsi) as T?
-        }
-    }
-
-    inline fun <reified T : HtmlElement> getElement(): T {
-        val instance: HtmlElement = newInstance(jsi)
-        return element.getElementsByTag(instance.tag).first()?.let {
-            parseElement(it, jsi) as? T?
-        } ?: newInstance(jsi)
-    }
-
-    fun attr(attrName: String): String = element.attr(attrName)
-
-    fun attr(attrName: String, value: Any?) {
-        element.attr(attrName, value.toString())
-    }
-
-    override fun toString(): String {
-        return element.toString()
-    }
-
-    companion object {
-        fun parseElement(element: Element, jsi: JSI): HtmlElement {
-            return when (element.tag().toString()) {
-                "a" -> HtmlAnchor(element, jsi)
-                "body" -> HtmlBody(element, jsi)
-                "div" -> HtmlDiv(element, jsi)
-                "head" -> HtmlHead(element, jsi)
-                "img" -> HtmlImage(element, jsi)
-                "link" -> HtmlLink(element, jsi)
-                "meta" -> HtmlMeta(element, jsi)
-                "noscript" -> HtmlNoScript(element, jsi)
-                "script" -> HtmlScript(element, jsi)
-                "source" -> HtmlSource(element, jsi)
-                "title" -> HtmlTitle(element, jsi)
-                "video" -> HtmlVideo(element, jsi)
-                else -> object : HtmlElement(element, jsi) {
-                    override val tag: String get() = ""
-                }
-            }
-        }
-    }
-}

+ 0 - 15
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlHead.kt

@@ -1,15 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-
-class HtmlHead(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "head"
-
-    val title: HtmlTitle get() = getElement()
-
-    val meta: List<HtmlMeta> get() = getAllElements()
-    val links: List<HtmlLink> get() = getAllElements()
-    val scripts: List<HtmlScript> get() = getAllElements()
-    val noScript: List<HtmlNoScript> get() = getAllElements()
-}

+ 0 - 15
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlImage.kt

@@ -1,15 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-import org.mjdev.tvlib.webclient.html.HtmlElement
-
-class HtmlImage(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "img"
-
-    val src get() = attr("src")
-    val alt get() = attr("alt")
-
-    val width get() = attr("width") // todo css
-    val height get() = attr("height") // todo css
-}

+ 0 - 12
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlLink.kt

@@ -1,12 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-import org.mjdev.tvlib.webclient.html.HtmlElement
-
-class HtmlLink(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "link"
-
-    val rel: String get() = attr("rel")
-    val href: String get() = attr("href")
-}

+ 0 - 12
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlMeta.kt

@@ -1,12 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-
-@Suppress("MemberVisibilityCanBePrivate", "unused")
-class HtmlMeta(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "meta"
-
-    val charset: String get() = attr("charset")
-    val content: String get() = attr("content")
-}

+ 0 - 10
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlNoScript.kt

@@ -1,10 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-
-class HtmlNoScript(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "noscript"
-
-    val links: List<HtmlLink> get() = getAllElements()
-}

+ 0 - 20
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlPage.kt

@@ -1,20 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-
-@Suppress("MemberVisibilityCanBePrivate")
-class HtmlPage(val url: String?, val html: String?, val jsi: JSI) {
-    val documentElement get() = HtmlDocument(html, jsi)
-
-    val head: HtmlHead get() = documentElement.head
-    val body: HtmlBody get() = documentElement.body
-
-    val title get() = head.title.text
-
-    val videos: List<HtmlVideo> get() = body.getAllElements()
-    val anchors: List<HtmlAnchor> get() = body.getAllElements()
-
-    override fun toString(): String {
-        return "${HtmlPage::class.simpleName} : ${url ?: ""}"
-    }
-}

+ 0 - 13
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlScript.kt

@@ -1,13 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-
-@Suppress("unused")
-class HtmlScript(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "script"
-
-    val type: String get() = attr("type")
-    val async: String get() = attr("async")
-    val src: String get() = attr("src")
-}

+ 0 - 11
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlSource.kt

@@ -1,11 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-import org.mjdev.tvlib.webclient.html.HtmlElement
-
-class HtmlSource(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "source"
-
-    val src: String get() = attr("src")
-}

+ 0 - 11
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlTitle.kt

@@ -1,11 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-import org.mjdev.tvlib.webclient.html.HtmlElement
-
-class HtmlTitle(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "title"
-
-    val text: String get() = ownText()
-}

+ 0 - 98
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/html/HtmlVideo.kt

@@ -1,98 +0,0 @@
-package org.mjdev.tvlib.webclient.html
-
-import android.webkit.MimeTypeMap
-import org.mjdev.tvlib.webclient.javascript.JSI
-import org.jsoup.nodes.Element
-
-@Suppress("unused", "SameParameterValue", "MemberVisibilityCanBePrivate")
-class HtmlVideo(element: Element, jsi: JSI) : HtmlElement(element, jsi) {
-    override val tag: String get() = "video"
-
-    val playsInline: String get() = attr("playsinline")
-    val poster: String get() = attr("poster")
-    val controlsList: String get() = attr("controlslist")
-    val preload: String get() = attr("preload")
-    val tabindex: String get() = attr("tabindex")
-    val controls: String get() = attr("controls")
-
-    val sources: List<String>
-        get() = mutableListOf<String>().apply {
-            if (attr("src").isNotEmpty()) add(attr("src"))
-            childs.filterIsInstance<HtmlSource>().let { list ->
-                if (list.isEmpty()) {
-                    listOf(attr("src"))
-                } else {
-                    list.map { htmlSource ->
-                        htmlSource.src
-                    }
-                }
-            }.also { list ->
-                addAll(list)
-            }
-        }
-
-    val videoStreams: List<String>
-        get() = sources.filter {
-            isVideoLink(it)
-        }
-
-    val hasStreams: Boolean get() = videoStreams.isNotEmpty()
-
-    companion object {
-
-        private val VIDEO_EXTENSIONS = listOf(
-            "mp4",
-            "m3u8",
-            "hls",
-            "avi",
-            "mpg",
-            "mpeg",
-            "avi"
-        )
-
-        private val VIDEO_EXCLUDE_EXTENSIONS = listOf(
-            "jpg",
-            "jpeg",
-            "js",
-            "ico",
-            "png",
-            "webp",
-            "css"
-        )
-
-        fun String.nonVideoExcluded(): Boolean {
-            val ext = MimeTypeMap.getFileExtensionFromUrl(this)
-            return !VIDEO_EXCLUDE_EXTENSIONS.contains(ext)
-        }
-
-        fun <T : List<String>> T.sortVideosByImportance(): List<String> {
-            val pairs = map {
-                Pair(indexOf(VIDEO_EXTENSIONS, it), it)
-            }
-            val filtered = pairs.filter {
-                (it.first >= 0) && it.second.nonVideoExcluded()
-            }
-            val sorted = filtered.sortedBy { p -> p.first }
-            return sorted.map { p -> p.second }
-        }
-
-        private fun indexOf(
-            where: List<String>,
-            what: String
-        ): Int {
-            for (i in where.indices) {
-                if (what.contains(where[i])) {
-                    return i
-                }
-            }
-            return -1
-        }
-
-        fun isVideoLink(url: String?): Boolean {
-            val isExtOK: Boolean = url?.nonVideoExcluded() ?: true
-            return isExtOK && url?.let { u ->
-                VIDEO_EXTENSIONS.map { ext -> (u.contains(ext)) }.isNotEmpty()
-            } ?: false
-        }
-    }
-}

+ 0 - 91
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/javascript/JSI.kt

@@ -1,91 +0,0 @@
-package org.mjdev.tvlib.webclient.javascript
-
-import android.webkit.JavascriptInterface
-import org.mjdev.tvlib.webclient.WebClient
-import org.mjdev.tvlib.webclient.html.HtmlPage
-import timber.log.Timber
-
-@Suppress("SameParameterValue", "MemberVisibilityCanBePrivate")
-class JSI(
-    val webView: WebClient,
-    val onGotHtml : (htmlPage: HtmlPage, loadedResources: List<String>) -> Unit
-) {
-    val name: String get() = JSI::class.simpleName!!.lowercase()
-
-    init {
-        webView.addJavascriptInterface(this, name)
-    }
-
-    // ---------------------------------------------------------------------------------------------
-
-    private fun callJavascript(s: String) {
-        Timber.e("Calling javascript command: $s")
-        webView.loadUrl("javascript:$s")
-    }
-
-    // ---------------------------------------------------------------------------------------------
-
-    private fun callJSI(fnc: String, vararg params: String?) = callJavascript(
-        StringBuilder().apply {
-            append("jsi.$fnc(")
-            params.forEach { p ->
-                append("$p${if (p == params.last()) "" else ","}")
-            }
-            append(");")
-        }.toString()
-    )
-
-    // ---------------------------------------------------------------------------------------------
-
-    fun getHtml() {
-        callJSI(
-            "onHtml",
-            "'<html>'+document.getElementsByTagName('html')[0].innerHTML+'</html>'"
-        )
-    }
-
-    // ---------------------------------------------------------------------------------------------
-
-    @JavascriptInterface
-    fun onHtml(html: String?) {
-        onGotHtml(
-            HtmlPage(
-                webView.url,
-                html,
-                webView.webViewClient.jsi
-            ),
-            webView.webViewClient.loadedResources
-        )
-    }
-
-//    @JavascriptInterface
-//    fun showToast(text: String?) {
-//        onUI {
-//            Toast.makeText(webView.context, text.toString(), Toast.LENGTH_LONG)
-//        }
-//    }
-
-    // ---------------------------------------------------------------------------------------------
-
-//    class Video(val jsi: JSI) {
-//        fun play(ve: VideoElement?) {
-//            val id = ve?.id
-//            val idx = ve?.idx ?: 0
-//            if (id == null) {
-//                jsi.callJavascript("document.getElementsByTagName(\"video\")[$idx].play();")
-//            } else {
-//                jsi.callJavascript("document.getElementById(\"$id\").play();")
-//            }
-//        }
-//
-//        fun fullScreen(ve: VideoElement?) {
-//            val id = ve?.id
-//            val idx = ve?.idx ?: 0
-//            if (id == null) {
-//                jsi.callJavascript("document.getElementsByTagName(\"video\")[$idx].webkitRequestFullscreen();")
-//            } else {
-//                jsi.callJavascript("var video = document.getElementById(\"$id\").webkitRequestFullscreen();")
-//            }
-//        }
-//    }
-}

+ 0 - 7
tvlib/src/main/kotlin/org/mjdev/tvlib/webclient/javascript/JSIListener.kt

@@ -1,7 +0,0 @@
-//package org.mjdev.tvlib.webclient.javascript
-
-//import org.mjdev.tvlib.webclient.html.HtmlPage
-
-//interface JSIListener {
-//    fun onGotHtml(htmlPage: HtmlPage, loadedResources: List<String>)
-//}

+ 124 - 130
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/WebVideoScrapper.kt

@@ -1,130 +1,124 @@
-///*
-// *  Copyright (c) Milan Jurkulák 2023.
-// *  Contact:
-// *  e: mimoccc@gmail.com
-// *  e: mj@mjdev.org
-// *  w: https://mjdev.org
-// */
-//
-//package org.mjdev.tvlib.webscrapper
-//
-//import androidx.core.net.toUri
-//import com.gargoylesoftware.htmlunit.BrowserVersion
-//import it.skrape.core.htmlDocument
-//import it.skrape.fetcher.BrowserFetcher
-//import it.skrape.fetcher.response
-//import it.skrape.fetcher.skrape
-//import it.skrape.selects.DomTreeElement
-//import org.jsoup.nodes.Document
-//import timber.log.Timber
-//
-//class WebVideoScrapper(
-//    private val baseUrls: List<String>,
-//    private val onVideoFound: (video: Video) -> Unit = {},
-//    private val onFinish: (videos: List<Video>) -> Unit = {}
-//) {
-//
-//    private val links: ArrayDeque<String> = ArrayDeque<String>().apply {
-//        addAll(baseUrls)
-//    }
-//
-//    private val videos = mutableListOf<Video>()
-//
-//    fun start() {
-//        while (links.isNotEmpty()) {
-//            val uri = links.removeFirstOrNull()
-//            if (uri != null) {
-//                Timber.e("Parsing :  $uri")
-//                parse(uri)
-//            }
-//        }
-//        onFinish(videos)
-//    }
-//
-//    private fun parse(uri: String) {
-//        try {
-//            skrape(BrowserFetcher) {
-//                request {
-//                    url = uri
-//                    sslRelaxed = true
-//                    followRedirects = true
-//                    timeout = 5000
-//                    userAgent = BrowserVersion.BEST_SUPPORTED.userAgent
-//                }
-//                response {
-//                    htmlDocument {
-//                        eachVideo.forEach { src ->
-//                            val url = normalizedUrl("${document.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
-//                            )
-//                            videos.add(video)
-//                            onVideoFound(video)
-//                        }
-//                        eachLink.values.forEach { href ->
-//                            links.add(normalizedUrl("${document.baseUri()}/$href"))
-//                        }
-//                    }
-//                }
-//            }
-//        } catch (e: Exception) {
-//            Timber.e(e)
-//        }
-//    }
-//
-//    // todo : intelligent
-//    @Suppress("UNUSED_PARAMETER")
-//    private fun parseThumb(document: Document, url: String): String {
-//        return url
-//    }
-//
-//    // todo : intelligent
-//    @Suppress("UNUSED_PARAMETER")
-//    private fun parseCategory(document: Document, url: String): String {
-//        val auth = document.location().toUri().authority.toString()
-//        return auth
-//    }
-//
-//    // todo : intelligent
-//    @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(u: String): String = u
-//        .replace("//", "/")
-//        .let { url ->
-//            if ((!url.startsWith("https://")) && url.startsWith("https:/")) {
-//                url.replace("https:/", "https://")
-//            } else url
-//        }.let { url ->
-//            if ((!url.startsWith("http://")) && url.startsWith("http:/")) {
-//                url.replace("http:/", "http://")
-//            } else url
-//        }
-//
-//    data class Video(
-//        val url: String,
-//        val title: String,
-//        val category: String,
-//        val thumb: String
-//    )
-//}
-//
-//val DomTreeElement.eachVideo: List<String>
-//    get() {
-//        return allElements.filter { e ->
-//            (e.tagName == "video") || (e.tagName == "source")
-//        }.filter { e ->
-//            e.hasAttribute("src")
-//        }.map { e ->
-//            e.attribute("src")
-//        }
-//    }
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+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
+import org.mjdev.tvlib.webscrapper.adblock.IAdBlocker
+import org.mjdev.tvlib.webscrapper.adblock.NoAdBlock
+import org.mjdev.tvlib.webscrapper.fetcher.BrowserFetcher
+import org.mjdev.tvlib.webscrapper.parser.htmlDocument
+import timber.log.Timber
+
+class WebVideoScrapper(
+    private val context: Context,
+    private val baseUrls: List<String>,
+    private val addBlock: IAdBlocker = NoAdBlock(),
+    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 videos = mutableListOf<Video>()
+
+    fun start() {
+        while (links.isNotEmpty()) {
+            val uri = links.removeFirstOrNull()
+            if (uri != null) {
+                Timber.e("Parsing :  $uri")
+                parse(uri)
+            }
+        }
+        onFinish(videos)
+    }
+
+    private fun parse(uri: String) {
+        try {
+            skrape(fetcher) {
+                request {
+                    url = uri
+                    sslRelaxed = true
+                    followRedirects = true
+                    timeout = 5000
+                    userAgent = BrowserVersion.BEST_SUPPORTED.userAgent
+                }
+                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
+                            )
+                            videos.add(video)
+                            onVideoFound(video)
+                        }
+                        eachLink.values.forEach { href ->
+                            if ((!href.startsWith("#")) && (href != "/") && (!href.contentEquals(
+                                    baseUri
+                                ))
+                            ) {
+                                links.add(normalizedUrl(baseUri, href))
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (e: Exception) {
+            Timber.e(e)
+        }
+    }
+
+    // todo : intelligent
+    @Suppress("UNUSED_PARAMETER")
+    private fun parseThumb(document: Document, url: String): String {
+        return url
+    }
+
+    // todo : intelligent
+    @Suppress("UNUSED_PARAMETER")
+    private fun parseCategory(document: Document, url: String): String {
+        return document.location().toUri().authority.toString()
+    }
+
+    // todo : intelligent
+    @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"
+    }
+
+    data class Video(
+        val url: String,
+        val title: String,
+        val category: String,
+        val thumb: String
+    )
+}

+ 92 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/AdBlock.kt

@@ -0,0 +1,92 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.adblock
+
+import android.content.Context
+import android.net.Uri
+import org.mjdev.tvlib.extensions.StringExt.removeComments
+import org.mjdev.tvlib.extensions.StringExt.removeEmptyLines
+import org.mjdev.tvlib.extensions.StringExt.trimLines
+import org.mjdev.tvlib.helpers.http.MimeTypeMapUtils
+import timber.log.Timber
+import java.io.File
+import java.io.InputStream
+import java.net.URL
+
+@Suppress("unused")
+class AdBlock(
+    inputStream: InputStream,
+    private val blockMimeTypes : Boolean = false
+) : IAdBlocker {
+
+    constructor(file: File) : this(file.inputStream())
+
+    constructor(context: Context, fileName: String) : this(context.assets.open(fileName))
+
+    constructor(context: Context) : this(context, "adblock.txt")
+
+    private var hosts = mutableListOf<String>()
+    private val urls = mutableListOf<String>()
+
+    // todo may be another ime types
+    private val allowedMimeTypes = mutableListOf(
+        "text/html",
+        "text/plain",
+        "text/javascript",
+        "application/javascript",
+        "video/mp4",
+        "application/json",
+    )
+
+    init {
+        try {
+            inputStream
+                .bufferedReader()
+                .readLines()
+                .removeEmptyLines()
+                .trimLines()
+                .removeComments('#')
+                .map { line ->
+                    when (line[0]) {
+                        // host starts with '*'
+                        '*' -> hosts.add(line.substring(1))
+                        // url to block
+                        else -> urls.add(line)
+                    }
+                }
+        } catch (e: Exception) {
+            Timber.e(e)
+        }
+    }
+
+    @Synchronized
+    override fun isBlocked(reqUrl: String?): Boolean {
+        val host = try {
+            Uri.parse(reqUrl).host
+        } catch (e: Exception) {
+            Timber.e(e)
+            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 (host?.let { h -> hosts.contains(h) } == true) return true
+        return reqUrl?.let { url ->
+            urls.any { blockedURL -> url.startsWith(blockedURL) }
+        } ?: false
+    }
+
+    override fun isBlocked(reqUrl: URL?): Boolean =
+        isBlocked(reqUrl.toString())
+
+    override fun isBlocked(reqUrl: Uri?): Boolean =
+        isBlocked(reqUrl.toString())
+
+}

+ 53 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/AdBlockInterceptor.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.webscrapper.adblock
+
+import android.content.Context
+import android.text.TextUtils
+import okhttp3.Interceptor
+import okhttp3.Protocol
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
+import org.mjdev.tvlib.helpers.http.CacheType
+import java.io.IOException
+
+@Suppress("unused")
+class AdBlockInterceptor(
+    private val context: Context,
+    private val adBlock: IAdBlocker = NoAdBlock()
+) : Interceptor {
+
+    @Throws(IOException::class)
+    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)
+            if (!TextUtils.isEmpty(cache) && (cache == CacheType.NORMAL.ordinal.toString() + ""))
+                originResponse
+            else originResponse.newBuilder()
+                .removeHeader("pragma")
+                .removeHeader("Cache-Control")
+                .header("Cache-Control", "max-age=3153600000")
+                .build()
+        }
+    }
+
+    private fun emptyResponse(request: Request) = Response.Builder()
+        .request(request)
+        .code(200)
+        .message("OK")
+        .protocol(Protocol.HTTP_1_1)
+        .body("".toResponseBody())
+        .build()
+}

+ 18 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/IAdBlocker.kt

@@ -0,0 +1,18 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.adblock
+
+import android.net.Uri
+import java.net.URL
+
+interface IAdBlocker {
+    fun isBlocked(reqUrl: String?): Boolean
+    fun isBlocked(reqUrl: URL?): Boolean
+    fun isBlocked(reqUrl: Uri?): Boolean
+}

+ 21 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/adblock/NoAdBlock.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.webscrapper.adblock
+
+import android.net.Uri
+import java.net.URL
+
+@Suppress("unused")
+class NoAdBlock : IAdBlocker {
+    override fun isBlocked(reqUrl: String?): Boolean = false
+
+    override fun isBlocked(reqUrl: URL?): Boolean = false
+
+    override fun isBlocked(reqUrl: Uri?): Boolean = false
+}

+ 42 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/Authentication.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.tvlib.webscrapper.base
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import java.util.*
+
+interface Authentication {
+    fun toHeaderValue(): String
+    @RequiresApi(Build.VERSION_CODES.O)
+    fun String.base64Encoded(): String = Base64.getEncoder().encodeToString(toByteArray()).orEmpty()
+}
+
+@Suppress("unused")
+class BasicAuth(
+    private var username: String = "",
+    private var password: String = ""
+) : Authentication {
+    @RequiresApi(Build.VERSION_CODES.O)
+    override fun toHeaderValue(): String = "Basic ${"$username:$password".base64Encoded()}"
+}
+
+@Suppress("unused")
+class OAuth2(
+    var clientId: String = "",
+    var clientSecret: String = ""
+) : Authentication {
+    override fun toHeaderValue(): String = "Bearer TODO"
+}
+
+@Suppress("unused")
+fun basic(init: BasicAuth.() -> Unit): Authentication = BasicAuth().also(init)
+
+@Suppress("unused")
+fun oauth2(init: OAuth2.() -> Unit): Authentication = OAuth2().also(init)

+ 115 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/BodyBuilder.kt

@@ -0,0 +1,115 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.base
+
+import org.intellij.lang.annotations.Language
+
+class BodyBuilder {
+    var contentType: String = "text/plain"
+    var data: String = ""
+
+    fun json(@Language("JSON") json: String) {
+        contentType = JSON
+        data = json
+    }
+
+    fun xml(@Language("XML") xml: String) {
+        contentType = XML
+        data = xml
+    }
+
+    @Suppress("unused")
+    fun form(form: String) {
+        contentType = FORM
+        data = form
+    }
+
+    fun json(init: Json.() -> Unit) {
+        Json().also(init).toBody()
+    }
+
+    @Suppress("unused")
+    fun form(init: Form.() -> Unit) {
+        Form().also(init).toBody()
+    }
+
+    private fun Json.toBody() {
+        this@BodyBuilder.contentType = JSON
+        this@BodyBuilder.data = toString()
+    }
+
+    private fun Form.toBody() {
+        this@BodyBuilder.contentType = FORM
+        this@BodyBuilder.data = toString()
+    }
+}
+
+class Json {
+    private val elements = mutableListOf<Pair<String, String>>()
+
+    @Suppress("MemberNameEqualsClassName")
+    fun json(init: Json.() -> Unit): Json = Json().also(init)
+
+    infix fun String.to(list: List<*>) {
+        val value = list.joinToString(separator = ",", prefix = "[", postfix = "]") {
+            when (it) {
+                null -> "null"
+                is Number, is Json, is Boolean -> it.toString()
+                else -> """"$it""""
+            }
+        }
+        elements += Pair(this, value)
+    }
+
+    infix fun String.to(string: String?) {
+        elements += if (string == null) Pair(this, "null") else Pair(this, """"$string"""")
+    }
+
+    infix fun String.to(number: Number?) {
+        elements += Pair(this, number.toString())
+    }
+
+    infix fun String.to(json: Json?) {
+        elements += Pair(this, json.toString())
+    }
+
+    infix fun String.to(boolean: Boolean?) {
+        elements += Pair(this, boolean.toString())
+    }
+
+    override fun toString(): String =
+        elements.joinToString(separator = ",", prefix = "{", postfix = "}") { (key, value) ->
+            """"$key":$value"""
+        }
+}
+
+class Form {
+    private val elements = mutableListOf<Pair<String, String>>()
+
+    infix fun String.to(string: String?) {
+        elements += if (string == null) Pair(this, "null") else Pair(this, """$string""")
+    }
+
+    infix fun String.to(number: Number?) {
+        elements += Pair(this, number.toString())
+    }
+
+    infix fun String.to(boolean: Boolean?) {
+        elements += Pair(this, boolean.toString())
+    }
+
+    override fun toString(): String =
+        elements.joinToString(separator = "&") { (key, value) ->
+            """$key=$value"""
+        }
+}
+
+private const val JSON = "application/json"
+private const val XML = "text/xml"
+private const val FORM = "application/x-www-form-urlencoded"

+ 75 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/Cookie.kt

@@ -0,0 +1,75 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.base
+
+import java.util.*
+
+data class Cookie(
+    val name: String,
+    val value: String,
+    val expires: Expires,
+    val maxAge: Int?,
+    val domain: Domain,
+    val path: String = "/",
+    val sameSite: SameSite = SameSite.LAX,
+    val secure: Boolean = false,
+    val httpOnly: Boolean = false
+)
+
+enum class SameSite {
+    STRICT,
+    LAX,
+    NONE
+}
+
+sealed class Expires {
+    data object Session : Expires()
+    data class Date(val date: String) : Expires()
+}
+
+data class Domain(val domain: String, val includesSubdomains: Boolean)
+
+fun String.toCookie(origin: String): Cookie {
+    val attributes = this.split(";").map { it.trim() }
+    val (name, value) = attributes[0].split("=")
+
+    val path = attributes.getAttribute("path") ?: "/"
+    val expires = attributes.getAttribute("expires").toExpires()
+    val maxAge = attributes.getAttribute("max-age")?.toInt()
+    val domain = when (val domainUrl = attributes.getAttribute("domain")) {
+        null -> Domain(origin, false)
+        else -> Domain(domainUrl, true)
+    }
+    val sameSite = attributes.getAttribute("samesite").toSameSite()
+    val secure = attributes.any { it.lowercase(Locale.getDefault()) == "secure" }
+    val httpOnly = attributes.any { it.lowercase(Locale.getDefault()) == "httponly" }
+    return Cookie(name, value, expires, maxAge, domain, path, sameSite, secure, httpOnly)
+}
+
+private fun List<String>.getAttribute(attributeName: String) =
+    this.find { it.startsWith("${attributeName}=", ignoreCase = true) }?.takeLastWhile { it != '=' }
+
+private fun String?.toSameSite(): SameSite {
+    return when (this?.lowercase(Locale.getDefault())) {
+        "strict" -> SameSite.STRICT
+        "lax" -> SameSite.LAX
+        "none" -> SameSite.NONE
+        else -> SameSite.LAX
+    }
+}
+
+private fun String?.toExpires(): Expires {
+    return when (this) {
+        null -> Expires.Session
+        else -> Expires.Date(this)
+    }
+}
+
+val String.urlOrigin: String
+    get() = this.substringAfter("://").substringBefore("/").substringBefore(":")

+ 26 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/ProxyBuilder.kt

@@ -0,0 +1,26 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.base
+
+import java.net.InetSocketAddress
+import java.net.Proxy
+
+data class ProxyBuilder(
+    var type: Proxy.Type = Proxy.Type.HTTP,
+    var host: String = "",
+    var port: Int = 0
+) {
+    @Suppress("unused")
+    fun toProxy(): Proxy {
+        if (host.isBlank() || port == 0) {
+            return Proxy.NO_PROXY
+        }
+        return Proxy(type, InetSocketAddress(host, port))
+    }
+}

+ 64 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/Request.kt

@@ -0,0 +1,64 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.base
+
+import java.net.URI
+
+data class Request(
+    var method: Method = Method.GET,
+
+    var url: String = UrlBuilder().toString(),
+    var proxy: ProxyBuilder? = null,
+
+    var userAgent: String = "Mozilla/5.0 skrape.it",
+    var headers: Map<String, String> = emptyMap(),
+    var cookies: Map<String, String> = emptyMap(),
+    var timeout: Int = 5000,
+    var followRedirects: Boolean = true,
+    var authentication: Authentication? = null,
+    var body: String? = null,
+
+    var sslRelaxed: Boolean = false
+) {
+    fun url(init: UrlBuilder.() -> Unit) {
+        val formerUrl = URI(url)
+        url = UrlBuilder().apply {
+            protocol = protocol.findOrDefault(formerUrl.scheme)
+            host = formerUrl.host
+            port = formerUrl.port
+            path = formerUrl.path
+            fragment = formerUrl.fragment
+            formerUrl.query?.let { queryParam { +it } }
+        }.also(init).toString()
+    }
+
+    @Suppress("unused")
+    fun urlBuilder(init: UrlBuilder.() -> Unit): String {
+        return UrlBuilder().also(init).toString()
+    }
+
+    @Suppress("unused")
+    fun proxyBuilder(init: ProxyBuilder.() -> Unit): ProxyBuilder = ProxyBuilder().also(init)
+
+    @Suppress("unused")
+    fun body(init: BodyBuilder.() -> Unit) {
+        val invokedBody = BodyBuilder().also(init)
+        body = invokedBody.data
+        headers += "Content-Type" to invokedBody.contentType
+    }
+}
+
+enum class Method {
+    GET,
+    POST,
+    PUT,
+    DELETE,
+    PATCH,
+    HEAD
+}

+ 46 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/Result.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.tvlib.webscrapper.base
+
+@Suppress("unused")
+class Result(
+    val responseBody: String,
+    val responseStatus: Status,
+    val contentType: ContentType,
+    val headers: Map<String, String>,
+    val baseUri: String = "",
+    val cookies: List<Cookie>,
+) {
+    infix fun httpHeader(name: String): String? = this.headers[name]
+
+    fun httpHeaders(init: Map<String, String>.() -> Unit): Map<String, String> {
+        headers.apply(init)
+        return headers
+    }
+
+    fun httpHeader(name: String, init: String?.() -> Unit): String? {
+        val header = headers[name]
+        header.apply(init)
+        return header
+    }
+
+    fun <T> status(init: Status.() -> T): T {
+        return responseStatus.init()
+    }
+
+    fun cookies(init: List<Cookie>.() -> Unit): List<Cookie> =
+        cookies.apply(init)
+
+    data class Status(
+        val code: Int,
+        val message: String,
+    )
+}
+
+typealias ContentType = String?

+ 98 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/Scraper.kt

@@ -0,0 +1,98 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.base
+
+import kotlinx.coroutines.runBlocking
+import org.mjdev.tvlib.webscrapper.fetcher.BlockingFetcher
+import org.mjdev.tvlib.webscrapper.fetcher.NonBlockingFetcher
+import kotlin.reflect.full.createInstance
+
+class Scraper<R>(
+    private val fetcher: NonBlockingFetcher<R>,
+    private val preparedRequest: R
+) {
+    constructor(
+        client: NonBlockingFetcher<R>
+    ) : this(client, client.requestBuilder)
+
+    suspend fun request(
+        init: suspend R.() -> Unit
+    ): Scraper<R> {
+        init(preparedRequest)
+        return this
+    }
+
+    suspend fun scrape(): Result =
+        fetcher.fetch(preparedRequest)
+}
+
+private class FetcherConverter<T>(
+    val blockingFetcher: BlockingFetcher<T>,
+) : NonBlockingFetcher<T> {
+
+    override suspend fun fetch(
+        request: T
+    ): Result = blockingFetcher.fetch(request)
+
+    override val requestBuilder: T
+        get() = blockingFetcher.requestBuilder
+}
+
+fun <R, T> skrape(
+    fetcher: BlockingFetcher<R>,
+    init: suspend Scraper<R>.() -> T
+): T = runBlocking {
+    Scraper(FetcherConverter(fetcher)).init()
+}
+
+@Suppress("unused")
+suspend fun <R, T> skrape(
+    fetcher: NonBlockingFetcher<R>,
+    init: suspend Scraper<R>.() -> T
+): T = Scraper(fetcher).init()
+
+@Suppress("unused")
+suspend fun Scraper<*>.expect(
+    result: suspend Result.() -> Unit
+) {
+    response(result)
+}
+
+@Suppress("unused")
+fun Scraper<*>.expectBlocking(
+    result: suspend Result.() -> Unit
+) {
+    runBlocking { response(result) }
+}
+
+@Suppress("unused")
+suspend fun <T> Scraper<*>.extract(
+    result: suspend Result.() -> T
+): T = response(result)
+
+suspend fun <T> Scraper<*>.response(
+    result: suspend Result.() -> T
+): T = scrape().result()
+
+@Suppress("unused")
+fun <T> Scraper<*>.extractBlocking(
+    result: suspend Result.() -> T
+): T = runBlocking { response(result) }
+
+@Suppress("unused")
+suspend inline fun <reified T : Any> Scraper<*>.extractIt(
+    crossinline result: Result.(T) -> Unit
+): T = with(T::class) {
+    response { createInstance().also { result(it) } }
+}
+
+@Suppress("unused")
+inline fun <reified T : Any> Scraper<*>.extractItBlocking(
+    crossinline result: Result.(T) -> Unit
+): T = runBlocking { extractIt(result) }

+ 100 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/base/UrlBuilder.kt

@@ -0,0 +1,100 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.base
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import org.mjdev.tvlib.webscrapper.base.UrlBuilder.Protocol
+
+data class UrlBuilder(
+    /**
+     * Defines the protocol of the URL the request is made against.
+     * Defaults to HTTP.
+     * @see Protocol for all possible values.
+     */
+    var protocol: Protocol = Protocol.HTTP,
+
+    /**
+     * Defines the hostname of the URL the request is made against.
+     * Defaults to "localhost".
+     */
+    var host: String = "localhost",
+
+    /**
+     * Defines the port of the URL the request is made against.
+     * Defaults to 8080.
+     */
+    var port: Int = 8080,
+
+    /**
+     * Defines the path of the URL the request is made against.
+     */
+    var path: String = "",
+
+    var fragment: String? = null,
+) {
+
+    private var queryParam: String = ""
+
+    fun queryParam(init: QueryParam.() -> Unit) {
+        queryParam = QueryParam().also(init).toString()
+    }
+
+    enum class Protocol(val value: String) {
+        HTTP("http"),
+        HTTPS("https"),
+        FTP("ftp"),
+        FILE("file"),
+        ;
+
+        internal fun findOrDefault(value: String): Protocol =
+            entries.find { it.value == value } ?: HTTP
+    }
+
+    override fun toString(): String = buildString {
+        append("${protocol.value}://")
+        append(host)
+        append(if (port < 0) "" else ":$port")
+        append(if (path.isNotBlank() && !path.startsWith("/")) "/$path" else path)
+        append(fragment?.let { "#$it" } ?: "")
+        append(queryParam)
+    }
+}
+
+class QueryParam {
+    private val params: MutableMap<String, String> = mutableMapOf()
+
+    @RequiresApi(Build.VERSION_CODES.N)
+    infix fun String.to(value: Any?) {
+        // TODO: do not ignore entries with value null
+        params.computeIfAbsent(this) {
+            when (value) {
+                null -> "null"
+                is List<*> -> value.joinToString(separator = ",")
+                else -> "$value"
+            }
+        }
+    }
+
+    private val keyOnlyParams: MutableList<String> = mutableListOf()
+    operator fun String.unaryPlus() {
+        keyOnlyParams += this
+    }
+
+    override fun toString(): String {
+        return buildString {
+            params.forEach { (key, value) ->
+                append("&$key=$value")
+            }
+            keyOnlyParams.forEach {
+                append("&$it")
+            }
+        }.replaceFirst("&", "?")
+    }
+}

+ 21 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/fetcher/BaseFetcher.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.webscrapper.fetcher
+
+import org.mjdev.tvlib.webscrapper.base.Result
+
+interface BlockingFetcher<T> {
+    fun fetch(request: T): Result
+    val requestBuilder: T
+}
+
+interface NonBlockingFetcher<T> {
+    suspend fun fetch(request: T): Result
+    val requestBuilder: T
+}

+ 209 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/fetcher/BrowserFetcher.kt

@@ -0,0 +1,209 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.fetcher
+
+import android.annotation.SuppressLint
+import android.content.Context
+import com.gargoylesoftware.htmlunit.*
+import com.gargoylesoftware.htmlunit.html.HtmlPage
+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.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.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"
+) : 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 httpLoggingInterceptor by lazy {
+        HttpLoggingInterceptor().apply {
+            level = HttpLoggingInterceptor.Level.BODY
+        }
+    }
+
+    //    private val adBlockInterceptor by lazy { AdBlockInterceptor(context, adBlock) }
+    private val httpClient by lazy {
+        OkHttpClient.Builder().apply {
+//            addNetworkInterceptor(adBlockInterceptor)
+            addNetworkInterceptor(userAgentInterceptor)
+            addNetworkInterceptor(cacheInterceptor)
+            addNetworkInterceptor(httpLoggingInterceptor)
+            cache(httpCache)
+        }.build()
+    }
+
+    override val requestBuilder: Request get() = Request()
+
+    override fun fetch(request: Request): Result {
+        val client = WebClient(BrowserVersion.BEST_SUPPORTED)
+            .withOptions(request)
+        val webRequest = WebRequest(
+            URL(request.url),
+            request.method.toHttpMethod()
+        ).apply {
+            request.body?.run {
+                requestBody = this
+            }
+        }
+        val page: Page = client.getPage(client.currentWindow.topWindow, webRequest)
+        val httpResponse = page.webResponse
+        val document = when {
+            page.isHtmlPage -> (page as HtmlPage).asXml()
+            else -> httpResponse.contentAsString
+        }
+        val headers = httpResponse.responseHeaders.toMap()
+        val result = Result(
+            responseBody = document,
+            responseStatus = httpResponse.toStatus(),
+            contentType = httpResponse.contentType,
+            headers = headers,
+            baseUri = request.url,
+            cookies = httpResponse.responseHeaders
+                .filter { it.name == "Set-Cookie" }
+                .map { it.value.toCookie(request.url.urlOrigin) }
+        )
+        client.javaScriptEngine.shutdown()
+        client.close()
+//        client.cache.clear()
+        return result
+    }
+
+    @SuppressLint("SetJavaScriptEnabled")
+    private fun WebClient.withOptions(request: Request) = apply {
+        cssErrorHandler = SilentCssErrorHandler()
+        ajaxController = NicelyResynchronizingAjaxController()
+        createCookies(request)
+        addRequestHeader("User-Agent", request.userAgent)
+        if (request.authentication != null) {
+            addRequestHeader("Authorization", request.authentication?.toHeaderValue())
+        }
+        request.headers.forEach {
+            addRequestHeader(it.key, it.value)
+        }
+        @Suppress("MagicNumber") waitForBackgroundJavaScript(10_000)
+        webConnection = OkHttpWebConnection()
+        options.apply {
+            timeout = request.timeout
+            isRedirectEnabled = request.followRedirects
+            maxInMemory = 0
+            isUseInsecureSSL = request.sslRelaxed
+            isCssEnabled = false
+            isPopupBlockerEnabled = true
+            isDownloadImages = false
+            isThrowExceptionOnScriptError = false
+            isThrowExceptionOnFailingStatusCode = false
+            isPrintContentOnFailingStatusCode = false
+            historySizeLimit = 0
+            historyPageCacheLimit = 0
+            isJavaScriptEnabled = true
+            withProxySettings(request)
+        }
+    }
+
+    private fun WebClientOptions.withProxySettings(request: Request): WebClientOptions {
+        if (request.proxy != null) {
+            proxyConfig = ProxyConfig(
+                request.proxy?.host,
+                request.proxy?.port ?: 80,
+                "http",
+                request.proxy?.type == Proxy.Type.SOCKS
+            )
+        }
+        return this
+    }
+
+    private fun WebClient.createCookies(
+        request: Request
+    ) {
+        request.cookies.forEach {
+            cookieManager.addCookie(createCookie(request.url, it.key, it.value))
+        }
+    }
+
+    private fun createCookie(
+        url: String,
+        name: String,
+        value: String
+    ): Cookie {
+        return Cookie(URL(url).host, name, value)
+    }
+
+    private fun WebResponse.toStatus() =
+        Result.Status(statusCode, statusMessage)
+
+    private inner class OkHttpWebConnection : WebConnection {
+        override fun close() {
+        }
+
+        override fun getResponse(wr: WebRequest?): WebResponse {
+            return httpClient.newCall(
+                okhttp3.Request.Builder().url(wr?.url?.toString() ?: "").build()
+            ).execute().let { r ->
+                WebResponseData(
+                    r.body.bytes(),
+                    r.code,
+                    r.message,
+                    r.headers.map { h ->
+                        NameValuePair(h.first, h.second)
+                    }
+                ).let { data ->
+                    WebResponse(data, wr, 0L)
+                }
+            }
+        }
+    }
+
+    companion object {
+        fun render(html: String): String =
+            WebClient(BrowserVersion.BEST_SUPPORTED).loadHtmlCodeIntoCurrentWindow(html).asXml()
+    }
+
+}
+
+@Suppress("unused")
+fun Map<String, String>.asRawCookieSyntax(): String =
+    entries.joinToString(";", postfix = ";") { "${it.key}=${it.value}" }
+
+fun List<NameValuePair>.toMap(): Map<String, String> =
+    associateBy({ it.name }, { it.value })
+
+fun Method.toHttpMethod(): HttpMethod = when (this) {
+    Method.GET -> HttpMethod.GET
+    Method.POST -> HttpMethod.POST
+    Method.HEAD -> HttpMethod.HEAD
+    Method.DELETE -> HttpMethod.DELETE
+    Method.PATCH -> HttpMethod.PATCH
+    Method.PUT -> HttpMethod.PUT
+}

+ 102 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/parser/Parser.kt

@@ -0,0 +1,102 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.parser
+
+import org.intellij.lang.annotations.Language
+import org.jsoup.nodes.Document
+import org.mjdev.tvlib.webscrapper.fetcher.BrowserFetcher
+import org.mjdev.tvlib.webscrapper.select.Doc
+import java.io.BufferedReader
+import java.io.File
+import java.io.InputStream
+import java.nio.charset.Charset
+import org.jsoup.parser.Parser.parse as jSoupParser
+import org.mjdev.tvlib.webscrapper.base.Result
+
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+internal class Parser(
+    var html: String,
+    val charset: Charset,
+    val jsExecution: Boolean,
+    val baseUri: String,
+) {
+
+    fun parse(): Doc {
+        return if (jsExecution) {
+            jSoupParser(BrowserFetcher.render(html), baseUri).toDocWrapper()
+        } else {
+            jSoupParser(html, baseUri).toDocWrapper()
+        }
+    }
+
+    private fun Document.toDocWrapper() = Doc(this)
+
+    class MissingDependencyException(message: String = "") : Exception(message)
+}
+
+@Suppress("unused")
+fun <T> htmlDocument(
+    @Language("HTML") html: String,
+    charset: Charset = Charsets.UTF_8,
+    jsExecution: Boolean = false,
+    baseUri: String = "",
+    init: Doc.() -> T,
+): T = htmlDocument(html, charset, jsExecution, baseUri).init()
+
+@Suppress("unused")
+fun <T> htmlDocument(
+    file: File,
+    charset: Charset = Charsets.UTF_8,
+    jsExecution: Boolean = false,
+    baseUri: String = "",
+    init: Doc.() -> T,
+): T = htmlDocument(file, charset, jsExecution, baseUri).init()
+
+@Suppress("unused")
+fun <T> htmlDocument(
+    bytes: InputStream,
+    charset: Charset = Charsets.UTF_8,
+    jsExecution: Boolean = false,
+    baseUri: String = "",
+    init: Doc.() -> T,
+): T = htmlDocument(bytes, charset, jsExecution, baseUri).init()
+
+fun htmlDocument(
+    @Language("HTML") html: String,
+    charset: Charset = Charsets.UTF_8,
+    jsExecution: Boolean = false,
+    baseUri: String = "",
+): Doc = Parser(html, charset, jsExecution, baseUri).parse()
+
+fun htmlDocument(
+    file: File,
+    charset: Charset = Charsets.UTF_8,
+    jsExecution: Boolean = false,
+    baseUri: String = "",
+): Doc = htmlDocument(file.readText(charset), charset, jsExecution, baseUri)
+
+fun htmlDocument(
+    bytes: InputStream,
+    charset: Charset = Charsets.UTF_8,
+    jsExecution: Boolean = false,
+    baseUri: String = "",
+): Doc = htmlDocument(bytes.bufferedReader().use(BufferedReader::readText), charset, jsExecution, baseUri)
+
+@Suppress("unused")
+val Result.document: Doc
+    get() = htmlDocument { this }
+
+fun <T> Result.htmlDocument(
+    executeJS: Boolean = false,
+    init: Doc.() -> T
+): T = htmlDocument(
+    html = responseBody,
+    baseUri = baseUri,
+    jsExecution = executeJS
+).init()

+ 89 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/CssSelectable.kt

@@ -0,0 +1,89 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select
+
+import org.jsoup.nodes.Element
+import java.util.*
+
+@Suppress("MemberVisibilityCanBePrivate")
+abstract class CssSelectable {
+    abstract val toCssSelector: String
+
+    internal abstract fun applySelector(rawCssSelector: String): List<DocElement>
+
+    fun <T> selection(cssSelector: String, init: CssSelector.() -> T): T =
+        CssSelector(rawCssSelector = cssSelector, doc = this).init()
+
+    operator fun <T> String.invoke(init: CssSelector.() -> T): T =
+        this@CssSelectable.selection(this, init)
+
+    open fun makeDefault(cssSelector: String, relaxed: Boolean = false): DocElement {
+        return DocElement(Element("${UUID.randomUUID()}"), relaxed)
+    }
+
+    infix fun findAll(cssSelector: String): List<DocElement> =
+        this.applySelector(cssSelector)
+
+    fun findByIndex(index: Int, cssSelector: String = ""): DocElement =
+        findAll(cssSelector).getOrElse(index) { makeDefault(cssSelector) }
+
+    operator fun Int.invoke(cssSelector: String = ""): DocElement =
+        findByIndex(this, cssSelector)
+
+    fun findBySelectorMatching(regex: Regex): List<DocElement> =
+        this@CssSelectable.findAll("*").filter { it.ownCssSelector.matches(regex) }
+
+    operator fun Regex.invoke(): List<DocElement> =
+        findBySelectorMatching(this)
+
+    infix fun findFirst(cssSelector: String): DocElement =
+        findByIndex(0, cssSelector)
+
+    fun findSecond(cssSelector: String = ""): DocElement =
+        findByIndex(1, cssSelector)
+
+    fun findThird(cssSelector: String = ""): DocElement =
+        findByIndex(2, cssSelector)
+
+    fun findLast(cssSelector: String = ""): DocElement =
+        findAll(cssSelector).last()
+
+    fun findSecondLast(cssSelector: String = ""): DocElement =
+        findAll(cssSelector).let { it.getOrElse(it.lastIndex - 1) { makeDefault(cssSelector) } }
+
+    fun <T> findAll(cssSelector: String = "", init: List<DocElement>.() -> T): T =
+        findAll(cssSelector).init()
+
+    fun <T> findByIndex(index: Int, cssSelector: String = "", init: DocElement.() -> T): T =
+        findByIndex(index, cssSelector).init()
+
+    operator fun <T> Int.invoke(cssSelector: String = "", init: DocElement.() -> T): T =
+        this(cssSelector).init()
+
+    fun <T> findBySelectorMatching(regex: Regex, init: List<DocElement>.() -> T): T =
+        findBySelectorMatching(regex).init()
+
+    operator fun <T> Regex.invoke(init: List<DocElement>.() -> T): T =
+        this().init()
+
+    fun <T> findFirst(cssSelector: String = "", init: DocElement.() -> T): T =
+        findFirst(cssSelector).init()
+
+    fun <T> findSecond(cssSelector: String = "", init: DocElement.() -> T): T =
+        findSecond(cssSelector).init()
+
+    fun <T> findThird(cssSelector: String = "", init: DocElement.() -> T): T =
+        findThird(cssSelector).init()
+
+    fun <T> findLast(cssSelector: String = "", init: DocElement.() -> T): T =
+        findLast(cssSelector).init()
+
+    fun <T> findSecondLast(cssSelector: String = "", init: DocElement.() -> T): T =
+        findSecondLast(cssSelector).init()
+}

+ 65 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/CssSelector.kt

@@ -0,0 +1,65 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select
+
+import org.jsoup.nodes.Document
+
+ @Suppress("MemberVisibilityCanBePrivate")
+ class CssSelector(
+     var rawCssSelector: String = "",
+     var withClass: CssClassName? = null,
+     var withId: String? = null,
+     var withAttributeKey: String? = null,
+     var withAttributeKeys: List<String>? = null,
+     var withAttribute: Pair<String, String>? = null,
+     var withAttributes: List<Pair<String, String>>? = null,
+     val doc: CssSelectable = Doc(Document(""))
+) : CssSelectable() {
+    override val toCssSelector: String
+        get() = ("${doc.toCssSelector} $this").trim()
+
+    override fun applySelector(rawCssSelector: String): List<DocElement> =
+        doc.applySelector("$this $rawCssSelector".trim())
+
+    override fun toString(): String = rawCssSelector.trim() + buildString {
+        append(withId.toIdSelector())
+        append(withClass.toClassesSelector())
+        append(withAttributeKey.toAttributeKeySelector())
+        append(withAttributeKeys.toAttributesKeysSelector())
+        append(withAttribute.toAttributeSelector())
+        append(withAttributes.toAttributesSelector())
+    }
+
+    private fun String?.toIdSelector() = this?.let { "#$it" }.orEmpty().withoutSpaces()
+
+    private fun CssClassName?.toClassesSelector() = this?.let { ".$it" }.orEmpty().withoutSpaces()
+
+    private fun String?.toAttributeKeySelector() = this?.let { "[$it]" }.orEmpty().withoutSpaces()
+
+    private fun List<String>?.toAttributesKeysSelector() =
+        this?.joinToString(prefix = "['", separator = "']['", postfix = "']").orEmpty().withoutSpaces()
+
+    private fun Pair<String, String>?.toAttributeSelector() =
+        this?.let { "[${it.first.withoutSpaces()}='${it.second}']" }.orEmpty()
+
+    private fun List<Pair<String, String>>?.toAttributesSelector() =
+        this?.joinToString(separator = "") { it.toAttributeSelector() }.orEmpty()
+
+    private fun String.withoutSpaces() = replace("\\s".toRegex(), "")
+}
+
+typealias CssClassName = String
+
+infix fun CssClassName.and(
+    value: String
+): String = "$this.$value"
+
+infix fun Pair<String, String>.and(
+    pair: Pair<String, String>
+): MutableList<Pair<String, String>> = mutableListOf(this).apply { add(pair) }

+ 36 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/Doc.kt

@@ -0,0 +1,36 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select
+
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+
+class Doc(
+    val document: Document,
+    override var relaxed: Boolean = false
+) : DomTreeElement() {
+    override val element: Element
+        get() = this.document
+
+    @Suppress("unused")
+    val wholeText: String by lazy { document.wholeText() }
+
+    @Suppress("unused")
+    val titleText: String by lazy { document.title() }
+
+    override val toCssSelector: String = ""
+
+    override fun makeDefaultElement(cssSelector: String): DocElement {
+        return DocElement(Element(cssSelector), this.relaxed)
+    }
+
+    override fun makeDefault(cssSelector: String, relaxed: Boolean): DocElement {
+        return super.makeDefault(cssSelector, this.relaxed)
+    }
+}

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

@@ -0,0 +1,180 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select
+
+import org.jsoup.nodes.Element
+import java.util.*
+
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+class DocElement internal constructor(
+    override val element: Element,
+    override val relaxed: Boolean,
+) : DomTreeElement() {
+    constructor(element: Element) : this(element, false)
+
+    val tagName: String by lazy { element.tagName() }
+
+    val ownText: String by lazy { element.ownText() }
+
+    val wholeText: String by lazy { element.wholeText() }
+
+    val attributes: Map<String, String> by lazy {
+        element.attributes().associate { it.key to it.value }
+    }
+
+    val attributeKeys: List<String> by lazy { attributes.map { it.key } }
+
+    val attributeValues: List<String> by lazy { attributes.map { it.value } }
+
+    infix fun attribute(attributeKey: String): String = attributes[attributeKey].orEmpty()
+
+    fun hasAttribute(attributeKey: String): Boolean = attribute(attributeKey).isNotBlank()
+
+    val dataAttributes: Map<String, String> by lazy { attributes.filter { it.key.startsWith("data-") } }
+
+    val className: String by lazy { attribute("class").trim() }
+
+    val classNames: Set<String> by lazy { className.split(" ").filter { it.isNotBlank() }.toSet() }
+
+    fun hasClass(className: String): Boolean =
+        classNames.map { it.lowercase(Locale.getDefault()) }
+            .contains(className.lowercase(Locale.getDefault()))
+
+    val id: String by lazy { attribute("id").trim() }
+
+    val parents: List<DocElement> by lazy { element.parents().map { DocElement(it, this.relaxed) } }
+
+    fun <T> parents(init: List<DocElement>.() -> T): T = parents.init()
+
+    val parent: DocElement by lazy {
+        try {
+            parents.first()
+        } catch (e: NoSuchElementException) {
+            throw ElementNotFoundException("parent")
+        }
+    }
+
+    fun <T> parent(init: DocElement.() -> T): T = parent.init()
+
+    val siblings: List<DocElement> by lazy {
+        element.siblingElements().map { DocElement(it, this.relaxed) }
+    }
+
+    fun <T> siblings(init: List<DocElement>.() -> T): T = siblings.init()
+
+    val isPresent: Boolean by lazy { allElements.isNotEmpty() }
+
+    val isNotPresent: Boolean by lazy { !isPresent }
+
+    override val toCssSelector: String
+        get() = element.cssSelector()
+
+    val parentsCssSelector: String by lazy {
+        parents {
+            when {
+                isNotEmpty() -> reversed().joinToString(separator = " > ") { it.tagName }
+                else -> ""
+            }
+        }
+    }
+
+    val ownCssSelector: String by lazy {
+        fun String.orNull(): String? = ifBlank { null }
+        fun List<String>.orNull(): List<String>? = ifEmpty { null }
+        CssSelector(
+            rawCssSelector = tagName,
+            withClass = classNames.joinToString(separator = ".").orNull(),
+            withId = id.orNull(),
+            withAttributes = attributes
+                .filterNot { it.key == "id" }
+                .filterNot { it.key == "class" }
+                .filterNot { it.value.isBlank() }
+                .toList(),
+            withAttributeKeys = attributes.filterValues { it.isBlank() }.map { it.key }.orNull(),
+        ).toString()
+    }
+
+    override fun makeDefault(cssSelector: String, relaxed: Boolean): DocElement {
+        return super.makeDefault(cssSelector, this.relaxed)
+    }
+}
+
+val List<DocElement>.text: String
+    get(): String = joinToString(separator = " ") { it.text }
+
+val List<DocElement>.html: String
+    get(): String = joinToString(separator = "\n") { it.outerHtml }
+
+val List<DocElement>.isPresent: Boolean
+    get(): Boolean = isNotEmpty()
+
+@Suppress("unused")
+val List<DocElement>.isNotPresent: Boolean
+    get(): Boolean = !isPresent
+
+@Suppress("unused")
+val List<DocElement>.eachText: List<String>
+    get(): List<String> = map { it.text }
+
+@Suppress("unused")
+val List<DocElement>.eachTagName: List<String>
+    get(): List<String> = map { it.tagName }
+
+@Suppress("unused")
+val List<DocElement>.eachAttribute: Map<String, String>
+    get() = map { it.attributes }.flatMap { it.toList() }.toMap()
+
+@Suppress("unused")
+val List<DocElement>.eachDataAttribute: Map<String, String>
+    get() = map { it.dataAttributes }.flatMap { it.toList() }.toMap()
+
+@Suppress("unused")
+infix fun List<DocElement>.attribute(attributeKey: String): String =
+    filter { it.hasAttribute(attributeKey) }
+        .joinToString { it.attribute(attributeKey) }
+
+@Suppress("unused")
+infix fun List<DocElement>.eachAttribute(attributeKey: String): List<String> =
+    map { it attribute attributeKey }
+        .filter { it.isNotEmpty() }
+
+@Suppress("unused")
+val List<DocElement>.eachClassName: List<String>
+    get(): List<String> = flatMap { it.classNames }.distinct()
+
+@Suppress("unused")
+val List<DocElement>.eachHref: List<String>
+    get(): List<String> = eachAttribute("href")
+
+@Suppress("unused")
+val List<DocElement>.eachSrc: List<String>
+    get(): List<String> = eachAttribute("src")
+
+@Suppress("unused")
+val List<DocElement>.eachLink: Map<String, String>
+    get(): Map<String, String> =
+        filter { it.hasAttribute("href") }
+            .associate { it.text to it.attribute("href") }
+
+@Suppress("unused")
+val List<DocElement>.eachImage: Map<String, String>
+    get(): Map<String, String> =
+        filter { it.tagName == "img" }
+            .filter { it.hasAttribute("src") }
+            .associate { it.attribute("alt") to it.attribute("src") }
+
+@Suppress("unused")
+fun <T> List<DocElement>.forEachLink(init: (text: String, url: String) -> T) {
+    eachLink.forEach { init(it.key, it.value) }
+}
+
+@Suppress("unused")
+fun <T> List<DocElement>.forEachImage(init: (altText: String, url: String) -> T) {
+    eachImage.forEach { init(it.key, it.value) }
+}

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

@@ -0,0 +1,106 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select
+
+import org.jsoup.nodes.Element
+
+@Suppress("MemberVisibilityCanBePrivate")
+abstract class DomTreeElement : CssSelectable() {
+    abstract val element: Element
+
+    abstract val relaxed: Boolean
+
+    val text: String by lazy { element.text() }
+
+    val html: String by lazy { element.html() }
+
+    val outerHtml: String by lazy { element.outerHtml() }
+
+    val allElements: List<DocElement> by lazy {
+        element.allElements.map {
+            DocElement(
+                it,
+                this.relaxed
+            )
+        }
+    }
+
+    val children: List<DocElement> by lazy {
+        element.children().map { DocElement(it, this.relaxed) }
+    }
+
+    @Suppress("unused")
+    fun <T> children(init: List<DocElement>.() -> T): T = children.init()
+
+    fun eachAttribute(attributeKey: String): List<String> =
+        allElements.map { it attribute attributeKey }
+            .filter { it.isNotEmpty() }
+
+    @Suppress("unused")
+    val eachHref: List<String> by lazy {
+        eachAttribute("href").filter { it.isNotEmpty() }
+    }
+
+    @Suppress("unused")
+    val eachSrc: List<String> by lazy {
+        eachAttribute("src").filter { it.isNotEmpty() }
+    }
+
+    @Suppress("unused")
+    val eachLink: Map<String, String>
+        get(): Map<String, String> = allElements.filter {
+            it.hasAttribute("href")
+        }.associate {
+            it.text to it.attribute("href")
+        }
+
+    @Suppress("unused")
+    val eachImage: Map<String, String>
+        get(): Map<String, String> =
+            allElements.filter { it.tagName == "img" }
+                .filter { it.hasAttribute("src") }
+                .associate { it.attribute("alt") to it.attribute("src") }
+
+    val eachVideo: List<String>
+        get() {
+            val elements = allElements
+            val videos = elements.filter { e -> (e.tagName == "video") || (e.tagName == "source") }
+            val videosWithSource = videos.filter { e -> e.hasAttribute("src") }
+            // todo type
+            return videosWithSource.map { e -> e.attribute("src") }
+        }
+
+    open fun makeDefaultElement(cssSelector: String): DocElement {
+        return super.makeDefault(cssSelector, this.relaxed)
+    }
+
+    override fun makeDefault(cssSelector: String, relaxed: Boolean): DocElement {
+        return if (relaxed) makeDefaultElement(cssSelector) else throw ElementNotFoundException(
+            cssSelector
+        )
+    }
+
+    override fun applySelector(rawCssSelector: String): List<DocElement> {
+        if (rawCssSelector.isEmpty()) {
+            return allElements
+        }
+
+        val queried = element.children().select(rawCssSelector).map { DocElement(it, relaxed) }
+        val selected = queried.takeIf { it.isNotEmpty() }
+
+        return if (relaxed) selected.orEmpty() else selected ?: throw ElementNotFoundException(
+            rawCssSelector
+        )
+    }
+
+    override fun toString(): String = element.toString()
+}
+
+open class ElementNotFoundException(selector: String, tag: String = "") :
+    Exception("Could not find element \"$tag$selector\"")

+ 19 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/CustomTagSelectors.kt

@@ -0,0 +1,19 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.customTag(
+    tag: String,
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("$tag$cssSelector", init)

+ 24 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/DemarcatingEditsSelectors.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.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.del(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("del$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.ins(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("ins$cssSelector", init)

+ 60 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/EmbeddedContentSelectors.kt

@@ -0,0 +1,60 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.applet(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("applet$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.embed(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("embed$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.iframe(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("iframe$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.noembed(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("noembed$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.`object`(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("object$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.param(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("param$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.picture(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("picture$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.source(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("source$cssSelector", init)

+ 96 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/FormSelectors.kt

@@ -0,0 +1,96 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.button(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("button$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.datalist(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("datalist$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.fieldset(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("fieldset$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.form(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("form$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.input(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("input$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.label(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("label$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.legend(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("legend$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.meter(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("meter$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.optgroup(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("optgroup$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.option(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("option$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.output(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("output$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.progress(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("progress$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.select(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("select$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.textarea(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("textarea$cssSelector", init)

+ 42 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/InteractiveSelectors.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.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.details(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("details$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.dialog(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("dialog$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.menu(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("menu$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.menuitem(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("menuitem$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.summary(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("summary$cssSelector", init)

+ 18 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/MainRootSelectors.kt

@@ -0,0 +1,18 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.html(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("html$cssSelector", init)

+ 48 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/MediaSelectors.kt

@@ -0,0 +1,48 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.area(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("area$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.audio(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("audio$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.img(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("img$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.map(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("map$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.track(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("track$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.video(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("video$cssSelector", init)

+ 48 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/MetadataSelectors.kt

@@ -0,0 +1,48 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.base(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("base$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.head(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("head$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.link(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("link$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.meta(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("meta$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.style(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("style$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.title(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("title$cssSelector", init)

+ 30 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/ScriptingSelectors.kt

@@ -0,0 +1,30 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.script(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("script$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.canvas(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("canvas$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.noscript(
+    cssSelector: String = "",
+    init: CssSelector.() -> T
+): T = selection("noscript$cssSelector", init)

+ 76 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/SectioningSelectors.kt

@@ -0,0 +1,76 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.body(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("body$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.div(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("div$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.section(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("section$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.nav(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("nav$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.article(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("article$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.aside(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("aside$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.h1(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("h1$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.h2(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("h2$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.h3(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("h3$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.h4(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("h4$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.h5(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("h5$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.h6(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("h6$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.header(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("header$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.footer(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("footer$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.address(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("address$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.main(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("main$cssSelector", init)

+ 52 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/TableSelectors.kt

@@ -0,0 +1,52 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.caption(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("caption$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.col(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("col$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.colgroup(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("colgroup$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.table(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("table$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.tbody(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("tbody$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.td(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("td$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.tfoot(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("tfoot$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.th(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("th$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.thead(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("thead$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.tr(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("tr$cssSelector", init)

+ 64 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/TextContentSelectors.kt

@@ -0,0 +1,64 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.blockquote(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("blockquote$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.dd(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("dd$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.dir(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("dir$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.dl(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("dl$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.dt(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("dt$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.figcaption(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("figcaption$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.figure(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("figure$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.hr(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("hr$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.li(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("li$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.ol(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("ol$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.ul(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("ul$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.p(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("p$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.pre(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("pre$cssSelector", init)

+ 132 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/TextSemanticsSelectors.kt

@@ -0,0 +1,132 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.a(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("a$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.abbr(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("abbr$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.b(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("b$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.bdi(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("bdi$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.bdo(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("bdo$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.br(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("br$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.cite(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("cite$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.code(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("code$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.data(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("data$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.dfn(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("dfn$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.em(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("em$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.i(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("i$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.kbd(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("kbd$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.mark(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("mark$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.q(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("q$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.rb(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("rb$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.rtc(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("rtc$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.ruby(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("ruby$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.s(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("s$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.samp(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("samp$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.small(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("small$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.span(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("span$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.strong(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("strong$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.sub(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("sub$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.sup(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("sup$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.time(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("time$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.tt(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("tt$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.u(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("u$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.`var`(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("var$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.wbr(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("wbr$cssSelector", init)

+ 28 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/webscrapper/select/html5/WebComponentsSelectors.kt

@@ -0,0 +1,28 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.webscrapper.select.html5
+
+import org.mjdev.tvlib.webscrapper.select.CssSelectable
+import org.mjdev.tvlib.webscrapper.select.CssSelector
+
+@Suppress("unused")
+fun <T> CssSelectable.content(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("content$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.shadow(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("shadow$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.slot(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("slot$cssSelector", init)
+
+@Suppress("unused")
+fun <T> CssSelectable.template(cssSelector: String = "", init: CssSelector.() -> T): T =
+    selection("template$cssSelector", init)