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"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
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