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:
tvmon-dev
2026-04-15 20:11:25 +09:00
parent ba188894a1
commit f4db19329f
6 changed files with 107 additions and 35 deletions

View File

@@ -19,10 +19,21 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
release {
storeFile file('tvmon-release.keystore')
storePassword 'tvmon1234'
keyAlias 'tvmon'
keyPassword 'tvmon1234'
}
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
minifyEnabled false

View File

@@ -407,28 +407,30 @@ class TvmonScraper {
}
}
val episodes = mutableListOf<Episode>()
val episodes = mutableListOf<Episode>()
val videoLinks = mutableListOf<VideoLink>()
val seenEpisodeIds = mutableSetOf<String>()
val episodeLinks = doc.select(".next-ep-list-scroll .ep-item, .ep-item")
for (link in episodeLinks) {
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 allEpisodeLinks) {
val href = link.attr("href")
if (href.isBlank()) continue
if (!href.contains("/$seriesId/")) continue
val fullUrl = resolveUrl(href)
val episodeIdMatch = Pattern.compile("/(\\d+)/(\\d+)$").matcher(href)
if (!episodeIdMatch.find()) continue
val episodeId = episodeIdMatch.group(2) ?: ""
if (episodeId in seenEpisodeIds) continue
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 episodeNumMatch = Pattern.compile("(\\d+ 화|\\d+ 회|EP?\\d+|제?\\d+부?|\\d+)").matcher(linkText)
val episodeTitle = if (episodeNumMatch.find()) {
episodeNumMatch.group(1)

View File

@@ -77,6 +77,10 @@ class DetailsFragment : DetailsSupportFragment() {
}
}
override fun onDestroy() {
super.onDestroy()
}
private fun setupDetails() {
Log.w(TAG, "setupDetails: Setting up details UI")
val presenterSelector = ClassPresenterSelector()
@@ -236,6 +240,11 @@ class DetailsFragment : DetailsSupportFragment() {
}
private fun addEpisodesRow(episodes: List<Episode>) {
val existingRowIndex = findRowIndexByHeader(getString(R.string.episodes))
if (existingRowIndex >= 0) {
rowsAdapter.removeItems(existingRowIndex, 1)
}
val episodesRowAdapter = ArrayObjectAdapter(EpisodePresenter())
episodes.forEachIndexed { index, episode ->
Log.d(TAG, " Episode $index: ${episode.number} - ${episode.title}")
@@ -279,9 +288,14 @@ class DetailsFragment : DetailsSupportFragment() {
}
}
private fun addCastRow(cast: List<CastMember>) {
private fun addCastRow(cast: List<CastMember>) {
if (cast.isEmpty()) return
val existingRowIndex = findRowIndexByHeader("출연진")
if (existingRowIndex >= 0) {
rowsAdapter.removeItems(existingRowIndex, 1)
}
val castAdapter = ArrayObjectAdapter(CastPresenter())
cast.forEach { member ->
castAdapter.add(member)

View File

@@ -56,6 +56,11 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
loadData()
}
override fun onDestroy() {
handler.removeCallbacksAndMessages(null)
super.onDestroy()
}
private fun setupUI() {
Log.w(TAG, "setupUI: Setting up UI")
headersState = HEADERS_ENABLED
@@ -77,27 +82,62 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
onItemViewClickedListener = this
}
private fun handleRowSelection(position: Int) {
private fun handleRowSelection(position: Int) {
if (position == currentSelectedRowIndex) return
currentSelectedRowIndex = position
if (position >= 0 && position < rowsAdapter.size()) {
val row = rowsAdapter.get(position) as? ListRow ?: return
val categoryKey = findCategoryKeyForRow(position)
if (categoryKey != null && categoryLoading[categoryKey] != true) {
val currentPage = categoryPages[categoryKey] ?: 1
val maxPage = categoryMaxPage[categoryKey] ?: 1
Log.w(TAG, "handleRowSelection: $categoryKey currentPage=$currentPage maxPage=$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? {
for ((key, _) in categoryPages) {
val catIndex = TvmonScraper.CATEGORIES.keys.toList().indexOf(key)
@@ -113,34 +153,33 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
return null
}
private fun loadNextPage(categoryKey: String, page: Int) {
private fun loadNextPage(categoryKey: String, page: Int) {
if (categoryLoading[categoryKey] == true) return
categoryLoading[categoryKey] = true
lifecycleScope.launch {
try {
val result = scraper.getCategory(categoryKey, page)
Log.w(TAG, "loadNextPage: $categoryKey page $page success=${result.success}, items=${result.items.size}")
if (result.success && result.items.isNotEmpty()) {
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 }
val adapter = categoryRowAdapters[categoryKey]
adapter?.let {
result.items.forEach { item -> it.add(item) }
if (newItems.isNotEmpty()) {
items.addAll(newItems)
val adapter = categoryRowAdapters[categoryKey]
activity?.runOnUiThread {
adapter?.let {
newItems.forEach { item -> it.add(item) }
}
}
}
val loadedPage = page
categoryPages[categoryKey] = loadedPage
categoryPages[categoryKey] = page
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) {
Log.e(TAG, "Error loading page $page for $categoryKey", e)

Binary file not shown.

6
version.json Normal file
View 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"
}