From f4db19329f2f5c8022e370362d429a8ea738282c Mon Sep 17 00:00:00 2001 From: tvmon-dev Date: Wed, 15 Apr 2026 20:11:25 +0900 Subject: [PATCH] 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 --- tvmon-app/app/build.gradle | 11 +++ .../tvmon/data/scraper/TvmonScraper.kt | 20 ++-- .../tvmon/ui/detail/DetailsFragment.kt | 18 +++- .../com/example/tvmon/ui/main/MainFragment.kt | 87 +++++++++++++----- tvmon-app/app/tvmon-release.keystore | Bin 0 -> 2710 bytes version.json | 6 ++ 6 files changed, 107 insertions(+), 35 deletions(-) create mode 100644 tvmon-app/app/tvmon-release.keystore create mode 100644 version.json diff --git a/tvmon-app/app/build.gradle b/tvmon-app/app/build.gradle index c47f99a..2a79548 100644 --- a/tvmon-app/app/build.gradle +++ b/tvmon-app/app/build.gradle @@ -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 diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/data/scraper/TvmonScraper.kt b/tvmon-app/app/src/main/java/com/example/tvmon/data/scraper/TvmonScraper.kt index d546349..f8a8ddd 100644 --- a/tvmon-app/app/src/main/java/com/example/tvmon/data/scraper/TvmonScraper.kt +++ b/tvmon-app/app/src/main/java/com/example/tvmon/data/scraper/TvmonScraper.kt @@ -407,28 +407,30 @@ class TvmonScraper { } } - val episodes = mutableListOf() +val episodes = mutableListOf() val videoLinks = mutableListOf() val seenEpisodeIds = mutableSetOf() - - 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) diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/detail/DetailsFragment.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/detail/DetailsFragment.kt index e45e799..311eb89 100644 --- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/detail/DetailsFragment.kt +++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/detail/DetailsFragment.kt @@ -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) { + 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) { +private fun addCastRow(cast: List) { if (cast.isEmpty()) return - + + val existingRowIndex = findRowIndexByHeader("출연진") + if (existingRowIndex >= 0) { + rowsAdapter.removeItems(existingRowIndex, 1) + } + val castAdapter = ArrayObjectAdapter(CastPresenter()) cast.forEach { member -> castAdapter.add(member) diff --git a/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt b/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt index 9551900..0a7c595 100644 --- a/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt +++ b/tvmon-app/app/src/main/java/com/example/tvmon/ui/main/MainFragment.kt @@ -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) diff --git a/tvmon-app/app/tvmon-release.keystore b/tvmon-app/app/tvmon-release.keystore new file mode 100644 index 0000000000000000000000000000000000000000..eaf8699a0d87f3ce3235088cfbe096b8397888ca GIT binary patch literal 2710 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 tca&i7N?It_+>rnP literal 0 HcmV?d00001 diff --git a/version.json b/version.json new file mode 100644 index 0000000..7244dcc --- /dev/null +++ b/version.json @@ -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" +} \ No newline at end of file