Fix pagination, episode parsing, and infinite scroll issues
- Fix episode parsing to capture all episodes from various page structures - Add duplicate prevention for episodes and rows - Implement smooth scrolling with preload next page - Add Handler cleanup to prevent memory leaks - Create release keystore and signing config
This commit is contained in:
@@ -19,10 +19,21 @@ android {
|
|||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
storeFile file('tvmon-release.keystore')
|
||||||
|
storePassword 'tvmon1234'
|
||||||
|
keyAlias 'tvmon'
|
||||||
|
keyPassword 'tvmon1234'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
|
|||||||
@@ -411,11 +411,13 @@ class TvmonScraper {
|
|||||||
val videoLinks = mutableListOf<VideoLink>()
|
val videoLinks = mutableListOf<VideoLink>()
|
||||||
val seenEpisodeIds = mutableSetOf<String>()
|
val seenEpisodeIds = mutableSetOf<String>()
|
||||||
|
|
||||||
val episodeLinks = doc.select(".next-ep-list-scroll .ep-item, .ep-item")
|
val seriesId = seriesUrl.substringAfterLast("/").substringBefore("?")
|
||||||
|
val allEpisodeLinks = doc.select(".next-ep-list-scroll .ep-item, .ep-item, .bo_v_list li a, #bo_v_list li a, .list_body .item a, .ep-list a")
|
||||||
|
|
||||||
for (link in episodeLinks) {
|
for (link in allEpisodeLinks) {
|
||||||
val href = link.attr("href")
|
val href = link.attr("href")
|
||||||
if (href.isBlank()) continue
|
if (href.isBlank()) continue
|
||||||
|
if (!href.contains("/$seriesId/")) continue
|
||||||
|
|
||||||
val fullUrl = resolveUrl(href)
|
val fullUrl = resolveUrl(href)
|
||||||
|
|
||||||
@@ -426,7 +428,7 @@ class TvmonScraper {
|
|||||||
if (episodeId in seenEpisodeIds) continue
|
if (episodeId in seenEpisodeIds) continue
|
||||||
seenEpisodeIds.add(episodeId)
|
seenEpisodeIds.add(episodeId)
|
||||||
|
|
||||||
val titleEl = link.selectFirst(".ep-item-title")
|
val titleEl = link.selectFirst(".ep-item-title, .item-title, strong, span")
|
||||||
val linkText = titleEl?.text()?.trim() ?: link.text().trim()
|
val linkText = titleEl?.text()?.trim() ?: link.text().trim()
|
||||||
|
|
||||||
val episodeNumMatch = Pattern.compile("(\\d+ 화|\\d+ 회|EP?\\d+|제?\\d+부?|\\d+)").matcher(linkText)
|
val episodeNumMatch = Pattern.compile("(\\d+ 화|\\d+ 회|EP?\\d+|제?\\d+부?|\\d+)").matcher(linkText)
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ class DetailsFragment : DetailsSupportFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupDetails() {
|
private fun setupDetails() {
|
||||||
Log.w(TAG, "setupDetails: Setting up details UI")
|
Log.w(TAG, "setupDetails: Setting up details UI")
|
||||||
val presenterSelector = ClassPresenterSelector()
|
val presenterSelector = ClassPresenterSelector()
|
||||||
@@ -236,6 +240,11 @@ class DetailsFragment : DetailsSupportFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addEpisodesRow(episodes: List<Episode>) {
|
private fun addEpisodesRow(episodes: List<Episode>) {
|
||||||
|
val existingRowIndex = findRowIndexByHeader(getString(R.string.episodes))
|
||||||
|
if (existingRowIndex >= 0) {
|
||||||
|
rowsAdapter.removeItems(existingRowIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
val episodesRowAdapter = ArrayObjectAdapter(EpisodePresenter())
|
val episodesRowAdapter = ArrayObjectAdapter(EpisodePresenter())
|
||||||
episodes.forEachIndexed { index, episode ->
|
episodes.forEachIndexed { index, episode ->
|
||||||
Log.d(TAG, " Episode $index: ${episode.number} - ${episode.title}")
|
Log.d(TAG, " Episode $index: ${episode.number} - ${episode.title}")
|
||||||
@@ -282,6 +291,11 @@ class DetailsFragment : DetailsSupportFragment() {
|
|||||||
private fun addCastRow(cast: List<CastMember>) {
|
private fun addCastRow(cast: List<CastMember>) {
|
||||||
if (cast.isEmpty()) return
|
if (cast.isEmpty()) return
|
||||||
|
|
||||||
|
val existingRowIndex = findRowIndexByHeader("출연진")
|
||||||
|
if (existingRowIndex >= 0) {
|
||||||
|
rowsAdapter.removeItems(existingRowIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
val castAdapter = ArrayObjectAdapter(CastPresenter())
|
val castAdapter = ArrayObjectAdapter(CastPresenter())
|
||||||
cast.forEach { member ->
|
cast.forEach { member ->
|
||||||
castAdapter.add(member)
|
castAdapter.add(member)
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
|
|||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
handler.removeCallbacksAndMessages(null)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupUI() {
|
private fun setupUI() {
|
||||||
Log.w(TAG, "setupUI: Setting up UI")
|
Log.w(TAG, "setupUI: Setting up UI")
|
||||||
headersState = HEADERS_ENABLED
|
headersState = HEADERS_ENABLED
|
||||||
@@ -82,7 +87,6 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
|
|||||||
currentSelectedRowIndex = position
|
currentSelectedRowIndex = position
|
||||||
|
|
||||||
if (position >= 0 && position < rowsAdapter.size()) {
|
if (position >= 0 && position < rowsAdapter.size()) {
|
||||||
val row = rowsAdapter.get(position) as? ListRow ?: return
|
|
||||||
val categoryKey = findCategoryKeyForRow(position)
|
val categoryKey = findCategoryKeyForRow(position)
|
||||||
|
|
||||||
if (categoryKey != null && categoryLoading[categoryKey] != true) {
|
if (categoryKey != null && categoryLoading[categoryKey] != true) {
|
||||||
@@ -92,12 +96,48 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
|
|||||||
Log.w(TAG, "handleRowSelection: $categoryKey currentPage=$currentPage maxPage=$maxPage")
|
Log.w(TAG, "handleRowSelection: $categoryKey currentPage=$currentPage maxPage=$maxPage")
|
||||||
|
|
||||||
if (currentPage < maxPage) {
|
if (currentPage < maxPage) {
|
||||||
loadNextPage(categoryKey, currentPage + 1)
|
preloadNextPage(categoryKey, currentPage + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun preloadNextPage(categoryKey: String, page: Int) {
|
||||||
|
if (categoryLoading[categoryKey] == true) return
|
||||||
|
categoryLoading[categoryKey] = true
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val result = scraper.getCategory(categoryKey, page)
|
||||||
|
Log.w(TAG, "preloadNextPage: $categoryKey page $page success=${result.success}, items=${result.items.size}")
|
||||||
|
|
||||||
|
if (result.success && result.items.isNotEmpty()) {
|
||||||
|
val items = categoryItems.getOrPut(categoryKey) { mutableListOf() }
|
||||||
|
val existingUrls = items.map { it.url }.toSet()
|
||||||
|
val newItems = result.items.filter { it.url !in existingUrls }
|
||||||
|
|
||||||
|
if (newItems.isNotEmpty()) {
|
||||||
|
items.addAll(newItems)
|
||||||
|
|
||||||
|
val adapter = categoryRowAdapters[categoryKey]
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
adapter?.let {
|
||||||
|
newItems.forEach { item -> it.add(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryPages[categoryKey] = page
|
||||||
|
Log.w(TAG, "Preloaded page $page for $categoryKey, total items=${items.size}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error preloading page $page for $categoryKey", e)
|
||||||
|
} finally {
|
||||||
|
categoryLoading[categoryKey] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun findCategoryKeyForRow(rowIndex: Int): String? {
|
private fun findCategoryKeyForRow(rowIndex: Int): String? {
|
||||||
for ((key, _) in categoryPages) {
|
for ((key, _) in categoryPages) {
|
||||||
val catIndex = TvmonScraper.CATEGORIES.keys.toList().indexOf(key)
|
val catIndex = TvmonScraper.CATEGORIES.keys.toList().indexOf(key)
|
||||||
@@ -124,23 +164,22 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
|
|||||||
|
|
||||||
if (result.success && result.items.isNotEmpty()) {
|
if (result.success && result.items.isNotEmpty()) {
|
||||||
val items = categoryItems.getOrPut(categoryKey) { mutableListOf() }
|
val items = categoryItems.getOrPut(categoryKey) { mutableListOf() }
|
||||||
items.addAll(result.items)
|
val existingUrls = items.map { it.url }.toSet()
|
||||||
|
val newItems = result.items.filter { it.url !in existingUrls }
|
||||||
|
|
||||||
|
if (newItems.isNotEmpty()) {
|
||||||
|
items.addAll(newItems)
|
||||||
|
|
||||||
val adapter = categoryRowAdapters[categoryKey]
|
val adapter = categoryRowAdapters[categoryKey]
|
||||||
|
activity?.runOnUiThread {
|
||||||
adapter?.let {
|
adapter?.let {
|
||||||
result.items.forEach { item -> it.add(item) }
|
newItems.forEach { item -> it.add(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val loadedPage = page
|
categoryPages[categoryKey] = page
|
||||||
categoryPages[categoryKey] = loadedPage
|
|
||||||
Log.w(TAG, "Loaded page $page for $categoryKey, total items=${items.size}")
|
Log.w(TAG, "Loaded page $page for $categoryKey, total items=${items.size}")
|
||||||
|
|
||||||
val currentMaxPage = categoryMaxPage[categoryKey] ?: 1
|
|
||||||
if (loadedPage < currentMaxPage) {
|
|
||||||
handler.postDelayed({
|
|
||||||
loadNextPage(categoryKey, loadedPage + 1)
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error loading page $page for $categoryKey", e)
|
Log.e(TAG, "Error loading page $page for $categoryKey", e)
|
||||||
|
|||||||
BIN
tvmon-app/app/tvmon-release.keystore
Normal file
BIN
tvmon-app/app/tvmon-release.keystore
Normal file
Binary file not shown.
6
version.json
Normal file
6
version.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"versionCode": 1,
|
||||||
|
"versionName": "1.0.0",
|
||||||
|
"apkUrl": "https://git.webpluss.net/sanjeok77/NeFLIX_release/releases/download/v1.0.0/app-release.apk",
|
||||||
|
"updateMessage": "tvmon v1.0.0 - Android TV app with pagination fix and cast display improvements"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user