diff --git a/README.md b/README.md index 977bf177..ff3c03f3 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin #### Podcini.R 6.6 introduces the powerful feature of synthetic podcasts, enables the receiving/handling shared single media as well as playlist from Youtube and YT Music, for more see the Youtube section below or the changelogs. That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs) #### Podcini.R version 6.5 as a major step forward brings YouTube channels in the app. They can be searched, received from share, subscribed and played from within Podcini. For more see the Youtube section below or the changelogs -#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions. #### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions. +#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions. This project was developed from a fork of [AntennaPod]() as of Feb 5 2024. diff --git a/app/build.gradle b/app/build.gradle index 9f5cdbbe..95f3e2f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ android { testApplicationId "ac.mdiq.podcini.tests" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - versionCode 3020253 - versionName "6.7.0" + versionCode 3020254 + versionName "6.7.1" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequest.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequest.kt index 80e7821b..84b98ac8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequest.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequest.kt @@ -1,10 +1,9 @@ package ac.mdiq.podcini.net.download.service import ac.mdiq.podcini.net.utils.UrlChecker.prepareUrl -import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.EpisodeMedia +import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.showStackTrace import android.os.Bundle import android.os.Parcel import android.os.Parcelable @@ -27,6 +26,7 @@ class DownloadRequest private constructor( var size: Long = 0 private var statusMsg = 0 + // only used in tests constructor(destination: String, source: String, title: String, feedfileId: Long, feedfileType: Int, username: String?, password: String?, arguments: Bundle?, initiatedByUser: Boolean) : this(destination, source, title, feedfileId, feedfileType, null, username, password, false, arguments, initiatedByUser) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index 0a2514d3..2347b04b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -3,9 +3,9 @@ package ac.mdiq.podcini.net.download.service import ac.mdiq.podcini.R import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create +import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink -import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileEpisodeDownload import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs @@ -21,10 +21,10 @@ import ac.mdiq.podcini.storage.utils.ChapterUtils import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter import ac.mdiq.podcini.ui.utils.NotificationUtils -import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.config.ClientConfigurator import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent +import ac.mdiq.podcini.util.Logd +import ac.mdiq.podcini.util.config.ClientConfigurator import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent @@ -328,7 +328,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item)) // TODO: should use different event? if (broadcastUnreadStateUpdate) EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(item)) - if (needSynch()) { + if (isProviderConnected) { Logd(TAG, "enqueue synch") val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD).currentTimestamp().build() SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt index 7cafbaea..53486775 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt @@ -3,8 +3,8 @@ package ac.mdiq.podcini.net.feed import ac.mdiq.podcini.R import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.download.service.DefaultDownloaderFactory -import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create import ac.mdiq.podcini.net.download.service.DownloadRequest +import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create import ac.mdiq.podcini.net.feed.parser.FeedHandler import ac.mdiq.podcini.net.feed.parser.FeedHandler.FeedHandlerResult import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh @@ -46,8 +46,6 @@ import androidx.core.app.NotificationManagerCompat import androidx.media3.common.util.UnstableApi import androidx.work.* import androidx.work.Constraints.Builder -import com.annimon.stream.Collectors -import com.annimon.stream.Stream import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @@ -163,7 +161,7 @@ object FeedUpdateManager { feedsToUpdate.shuffle() // If the worker gets cancelled early, every feed has a chance to be updated } else { val feed = Feeds.getFeed(feedId) ?: return Result.success() - Logd(TAG, "doWork feed.downloadUrl: ${feed.downloadUrl}") + Logd(TAG, "doWork updating single feed: ${feed.title} ${feed.downloadUrl}") if (!feed.isLocalFeed) allAreLocal = false feedsToUpdate = mutableListOf(feed) // feedsToUpdate.add(feed) // Needs to be updatable, so no singletonList @@ -181,14 +179,13 @@ object FeedUpdateManager { feedsToUpdate.clear() return Result.success() } - private fun createNotification(toUpdate: List?): Notification { + private fun createNotification(titles: List?): Notification { val context = applicationContext var contentText = "" var bigText: String? = "" - if (toUpdate != null) { - contentText = context.resources.getQuantityString(R.plurals.downloads_left, - toUpdate.size, toUpdate.size) - bigText = Stream.of(toUpdate).map { feed: Feed? -> "• " + feed!!.title }.collect(Collectors.joining("\n")) + if (titles != null) { + contentText = context.resources.getQuantityString(R.plurals.downloads_left, titles.size, titles.size) + bigText = titles.map { "• $it" }.joinToString("\n") } return NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID.downloading.name) .setContentTitle(context.getString(R.string.download_notification_title_feeds)) @@ -199,6 +196,7 @@ object FeedUpdateManager { .addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label), WorkManager.getInstance(context).createCancelPendingIntent(id)) .build() } + override fun getForegroundInfoAsync(): ListenableFuture { return Futures.immediateFuture(ForegroundInfo(R.id.notification_updating_feeds, createNotification(null))) } @@ -218,10 +216,11 @@ object FeedUpdateManager { // Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show() return } + val titles = feedsToUpdate.map { it.title ?: "No title" }.toMutableList() var i = 0 while (i < feedsToUpdate.size) { if (isStopped) return - notificationManager.notify(R.id.notification_updating_feeds, createNotification(feedsToUpdate)) + notificationManager.notify(R.id.notification_updating_feeds, createNotification(titles)) val feed = unmanaged(feedsToUpdate[i++]) try { Logd(TAG, "updating local feed? ${feed.isLocalFeed} ${feed.title}") @@ -236,7 +235,7 @@ object FeedUpdateManager { val status = DownloadResult(feed.id, feed.title?:"", DownloadError.ERROR_IO_ERROR, false, e.message?:"") LogsAndStats.addDownloadStatus(status) } -// toUpdate.removeAt(0) + titles.removeAt(0) } } private fun refreshYoutubeFeed(feed: Feed) { @@ -283,23 +282,23 @@ object FeedUpdateManager { LogsAndStats.addDownloadStatus(downloader.result) return } - val feedSyncTask = FeedSyncTask(applicationContext, request) - val success = feedSyncTask.run() + val feedUpdateTask = FeedUpdateTask(applicationContext, request) + val success = feedUpdateTask.run() if (!success) { Logd(TAG, "update failed: unsuccessful") Feeds.persistFeedLastUpdateFailed(feed, true) - LogsAndStats.addDownloadStatus(feedSyncTask.downloadStatus) + LogsAndStats.addDownloadStatus(feedUpdateTask.downloadStatus) return } if (request.feedfileId == null) return // No download logs for new subscriptions // we create a 'successful' download log if the feed's last refresh failed val log = LogsAndStats.getFeedDownloadLog(request.feedfileId) - if (log.isNotEmpty() && !log[0].isSuccessful) LogsAndStats.addDownloadStatus(feedSyncTask.downloadStatus) + if (log.isNotEmpty() && !log[0].isSuccessful) LogsAndStats.addDownloadStatus(feedUpdateTask.downloadStatus) if (!request.source.isNullOrEmpty()) { when { !downloader.permanentRedirectUrl.isNullOrEmpty() -> Feeds.updateFeedDownloadURL(request.source, downloader.permanentRedirectUrl!!) - feedSyncTask.redirectUrl.isNotEmpty() && feedSyncTask.redirectUrl != request.source -> - Feeds.updateFeedDownloadURL(request.source, feedSyncTask.redirectUrl) + feedUpdateTask.redirectUrl.isNotEmpty() && feedUpdateTask.redirectUrl != request.source -> + Feeds.updateFeedDownloadURL(request.source, feedUpdateTask.redirectUrl) } } } @@ -393,7 +392,7 @@ object FeedUpdateManager { } } - class FeedSyncTask(private val context: Context, request: DownloadRequest) { + class FeedUpdateTask(private val context: Context, request: DownloadRequest) { private val task = FeedParserTask(request) private var feedHandlerResult: FeedHandlerResult? = null val downloadStatus: DownloadResult diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt index 5c85f6d5..64ed8e3e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt @@ -11,10 +11,6 @@ object SynchronizationQueueSink { // To avoid a dependency loop of every class to SyncService, and from SyncService back to every class. private var serviceStarterImpl = Runnable {} - fun needSynch() : Boolean { - return isProviderConnected - } - fun setServiceStarterImpl(serviceStarter: Runnable) { serviceStarterImpl = serviceStarter } @@ -34,7 +30,6 @@ object SynchronizationQueueSink { fun enqueueFeedAddedIfSyncActive(context: Context, downloadUrl: String) { if (!isProviderConnected) return - LockingAsyncExecutor.executeLockedAsync { SynchronizationQueueStorage(context).enqueueFeedAdded(downloadUrl) syncNow() @@ -43,7 +38,6 @@ object SynchronizationQueueSink { fun enqueueFeedRemovedIfSyncActive(context: Context, downloadUrl: String) { if (!isProviderConnected) return - LockingAsyncExecutor.executeLockedAsync { SynchronizationQueueStorage(context).enqueueFeedRemoved(downloadUrl) syncNow() @@ -52,7 +46,6 @@ object SynchronizationQueueSink { fun enqueueEpisodeActionIfSyncActive(context: Context, action: EpisodeAction) { if (!isProviderConnected) return - LockingAsyncExecutor.executeLockedAsync { SynchronizationQueueStorage(context).enqueueEpisodeAction(action) syncNow() @@ -61,7 +54,6 @@ object SynchronizationQueueSink { fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) { if (!isProviderConnected) return - val item_ = media.episodeOrFetch() if (item_?.feed?.isLocalFeed == true) return if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt index 2f89abda..5b12ae15 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt @@ -3,9 +3,9 @@ package ac.mdiq.podcini.storage.database import ac.mdiq.podcini.R import ac.mdiq.podcini.net.download.service.DownloadServiceInterface import ac.mdiq.podcini.net.feed.LocalFeedUpdater.updateFeed +import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink -import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying @@ -164,7 +164,7 @@ object Episodes { // Do full update of this feed to get rid of the episode if (episode.feed != null) updateFeed(episode.feed!!, context.applicationContext, null) } else { - if (needSynch()) { + if (isProviderConnected) { // Gpodder: queue delete action for synchronization val action = EpisodeAction.Builder(episode, EpisodeAction.DELETE).currentTimestamp().build() SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt index e4787eb5..a8268784 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt @@ -1,9 +1,9 @@ package ac.mdiq.podcini.storage.database import ac.mdiq.podcini.net.download.DownloadError +import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink -import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch import ac.mdiq.podcini.playback.base.VideoMode import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal @@ -196,142 +196,6 @@ object Feeds { * I.e. episodes are removed from the database if they are not in this episode list. * @return The updated Feed from the database if it already existed, or the new Feed from the parameters otherwise. */ -// @Synchronized -// fun updateFeed0(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? { -// Logd(TAG, "updateFeed called") -// var resultFeed: Feed? -// val unlistedItems: MutableList = ArrayList() -// -// // Look up feed in the feedslist -// val savedFeed = searchFeedByIdentifyingValueOrID(newFeed, true) -// if (savedFeed == null) { -// Logd(TAG, "Found no existing Feed with title ${newFeed.title}. Adding as new one.") -// Logd(TAG, "newFeed.episodes: ${newFeed.episodes.size}") -// resultFeed = newFeed -// try { -// addNewFeedsSync(context, newFeed) -// // Update with default values that are set in database -// resultFeed = searchFeedByIdentifyingValueOrID(newFeed) -// if (removeUnlistedItems) runBlocking { deleteEpisodes(context, unlistedItems).join() } -// } catch (e: InterruptedException) { e.printStackTrace() -// } catch (e: ExecutionException) { e.printStackTrace() } -// return resultFeed -// } -// -// Logd(TAG, "Feed with title " + newFeed.title + " already exists. Syncing new with existing one.") -// newFeed.episodes.sortWith(EpisodePubdateComparator()) -// if (newFeed.pageNr == savedFeed.pageNr) { -// if (savedFeed.compareWithOther(newFeed)) { -// Logd(TAG, "Feed has updated attribute values. Updating old feed's attributes") -// savedFeed.updateFromOther(newFeed) -// } -// } else { -// Logd(TAG, "New feed has a higher page number.") -// savedFeed.nextPageLink = newFeed.nextPageLink -// } -//// appears not useful -//// if (savedFeed.preferences != null && savedFeed.preferences!!.compareWithOther(newFeed.preferences)) { -//// Logd(TAG, "Feed has updated preferences. Updating old feed's preferences") -//// savedFeed.preferences!!.updateFromOther(newFeed.preferences) -//// } -// val priorMostRecent = savedFeed.mostRecentItem -// val priorMostRecentDate: Date? = priorMostRecent?.getPubDate() -// var idLong = Feed.newId() -// // Look for new or updated Items -// for (idx in newFeed.episodes.indices) { -// val episode = newFeed.episodes[idx] -// val possibleDuplicate = EpisodeAssistant.searchEpisodeGuessDuplicate(newFeed.episodes, episode) -// if (!newFeed.isLocalFeed && possibleDuplicate != null && episode !== possibleDuplicate) { -// // Canonical episode is the first one returned (usually oldest) -// addDownloadStatus(DownloadResult(savedFeed.id, episode.title ?: "", DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false, -// """ -// The podcast host appears to have added the same episode twice. Podcini still refreshed the feed and attempted to repair it. -// -// Original episode: -// ${EpisodeAssistant.duplicateEpisodeDetails(episode)} -// -// Second episode that is also in the feed: -// ${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)} -// """.trimIndent())) -// continue -// } -// var oldItem = searchEpisodeByIdentifyingValue(savedFeed.episodes, episode) -// if (!newFeed.isLocalFeed && oldItem == null) { -// oldItem = EpisodeAssistant.searchEpisodeGuessDuplicate(savedFeed.episodes, episode) -// if (oldItem != null) { -// Logd(TAG, "Repaired duplicate: $oldItem, $episode") -// addDownloadStatus(DownloadResult(savedFeed.id, -// episode.title ?: "", DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false, -// """ -// The podcast host changed the ID of an existing episode instead of just updating the episode itself. Podcini still refreshed the feed and attempted to repair it. -// -// Original episode: -// ${EpisodeAssistant.duplicateEpisodeDetails(oldItem)} -// -// Now the feed contains: -// ${EpisodeAssistant.duplicateEpisodeDetails(episode)} -// """.trimIndent())) -// oldItem.identifier = episode.identifier -// if (needSynch() && oldItem.isPlayed() && oldItem.media != null) { -// val durs = oldItem.media!!.getDuration() / 1000 -// val action = EpisodeAction.Builder(oldItem, EpisodeAction.PLAY) -// .currentTimestamp() -// .started(durs) -// .position(durs) -// .total(durs) -// .build() -// SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action) -// } -// } -// } -// if (oldItem != null) oldItem.updateFromOther(episode) -// else { -// Logd(TAG, "Found new episode: ${episode.title}") -// episode.feed = savedFeed -// episode.id = idLong++ -// episode.feedId = savedFeed.id -// if (episode.media != null) { -// episode.media!!.id = episode.id -// if (!savedFeed.hasVideoMedia && episode.media!!.getMediaType() == MediaType.VIDEO) savedFeed.hasVideoMedia = true -// } -// if (idx >= savedFeed.episodes.size) savedFeed.episodes.add(episode) -// else savedFeed.episodes.add(idx, episode) -// -// val pubDate = episode.getPubDate() -// if (pubDate == null || priorMostRecentDate == null || priorMostRecentDate.before(pubDate) || priorMostRecentDate == pubDate) { -// Logd(TAG, "Marking episode published on $pubDate new, prior most recent date = $priorMostRecentDate") -// episode.setNew() -// if (savedFeed.preferences?.autoAddNewToQueue == true) { -// val q = savedFeed.preferences?.queue -// if (q != null) runOnIOScope { addToQueueSync(false, episode, q) } -// } -// } -// } -// } -// // identify episodes to be removed -// if (removeUnlistedItems) { -// val it = savedFeed.episodes.toMutableList().iterator() -// while (it.hasNext()) { -// val feedItem = it.next() -// if (searchEpisodeByIdentifyingValue(newFeed.episodes, feedItem) == null) { -// unlistedItems.add(feedItem) -// it.remove() -// } -// } -// } -// // update attributes -// savedFeed.lastUpdate = newFeed.lastUpdate -// savedFeed.type = newFeed.type -// savedFeed.lastUpdateFailed = false -// resultFeed = savedFeed -// try { -// upsertBlk(savedFeed) {} -// if (removeUnlistedItems) runBlocking { deleteEpisodes(context, unlistedItems).join() } -// } catch (e: InterruptedException) { e.printStackTrace() -// } catch (e: ExecutionException) { e.printStackTrace() } -// return resultFeed -// } - @Synchronized fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? { Logd(TAG, "updateFeed called") @@ -365,11 +229,6 @@ object Feeds { Logd(TAG, "New feed has a higher page number.") savedFeed.nextPageLink = newFeed.nextPageLink } -// appears not useful -// if (savedFeed.preferences != null && savedFeed.preferences!!.compareWithOther(newFeed.preferences)) { -// Logd(TAG, "Feed has updated preferences. Updating old feed's preferences") -// savedFeed.preferences!!.updateFromOther(newFeed.preferences) -// } val priorMostRecent = savedFeed.mostRecentItem val priorMostRecentDate: Date? = priorMostRecent?.getPubDate() var idLong = Feed.newId() @@ -381,21 +240,6 @@ object Feeds { // Look for new or updated Items for (idx in newFeed.episodes.indices) { val episode = newFeed.episodes[idx] -// val possibleDuplicate = EpisodeAssistant.searchEpisodeGuessDuplicate(newFeed.episodes, episode) -// if (!newFeed.isLocalFeed && possibleDuplicate != null && episode !== possibleDuplicate) { -// // Canonical episode is the first one returned (usually oldest) -// addDownloadStatus(DownloadResult(savedFeed.id, episode.title ?: "", DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false, -// """ -// The podcast host appears to have added the same episode twice. Podcini still refreshed the feed and attempted to repair it. -// -// Original episode: -// ${EpisodeAssistant.duplicateEpisodeDetails(episode)} -// -// Second episode that is also in the feed: -// ${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)} -// """.trimIndent())) -// continue -// } var oldItem = savedFeedAssistant.searchEpisodeByIdentifyingValue(episode) if (!newFeed.isLocalFeed && oldItem == null) { oldItem = savedFeedAssistant.searchEpisodeGuessDuplicate(episode) @@ -413,7 +257,7 @@ object Feeds { ${EpisodeAssistant.duplicateEpisodeDetails(episode)} """.trimIndent())) oldItem.identifier = episode.identifier - if (needSynch() && oldItem.isPlayed() && oldItem.media != null) { + if (isProviderConnected && oldItem.isPlayed() && oldItem.media != null) { val durs = oldItem.media!!.getDuration() / 1000 val action = EpisodeAction.Builder(oldItem, EpisodeAction.PLAY) .currentTimestamp() @@ -616,16 +460,22 @@ object Feeds { } } - class FeedAssistant(val feed: Feed, val feedId: Long = 0L) { + // savedFeedId == 0L means saved feed + class FeedAssistant(val feed: Feed, val savedFeedId: Long = 0L) { val map = mutableMapOf() + val tag: String = if (savedFeedId == 0L) "Saved feed" else "New feed" - init { - for (e in feed.episodes) { + init { + val iterator = feed.episodes.iterator() + while (iterator.hasNext()) { + val e = iterator.next() if (!e.identifier.isNullOrEmpty()) { if (map.containsKey(e.identifier!!)) { - // TODO: add addDownloadStatus - Logd(TAG, "FeedAssistant init identifier duplicate: ${e.identifier} ${e.title}") - addDownloadStatus(e, map[e.identifier!!]!!) + Logd(TAG, "FeedAssistant init $tag identifier duplicate: ${e.identifier} ${e.title}") + if (savedFeedId > 0L) { + addDownloadStatus(e, map[e.identifier!!]!!) + iterator.remove() + } continue } map[e.identifier!!] = e @@ -633,9 +483,11 @@ object Feeds { val idv = e.identifyingValue if (idv != e.identifier && !idv.isNullOrEmpty()) { if (map.containsKey(idv)) { - // TODO: add addDownloadStatus - Logd(TAG, "FeedAssistant init identifyingValue duplicate: $idv ${e.title}") - addDownloadStatus(e, map[idv]!!) + Logd(TAG, "FeedAssistant init $tag identifyingValue duplicate: $idv ${e.title}") + if (savedFeedId > 0L) { + addDownloadStatus(e, map[idv]!!) + iterator.remove() + } continue } map[idv] = e @@ -643,9 +495,11 @@ object Feeds { val url = e.media?.getStreamUrl() if (url != idv && !url.isNullOrEmpty()) { if (map.containsKey(url)) { - // TODO: add addDownloadStatus - Logd(TAG, "FeedAssistant init url duplicate: $url ${e.title}") - addDownloadStatus(e, map[url]!!) + Logd(TAG, "FeedAssistant init $tag url duplicate: $url ${e.title}") + if (savedFeedId > 0L) { + addDownloadStatus(e, map[url]!!) + iterator.remove() + } continue } map[url] = e @@ -653,14 +507,16 @@ object Feeds { val title = canonicalizeTitle(e.title) if (title != idv && title.isNotEmpty()) { if (map.containsKey(title)) { - // TODO: add addDownloadStatus val episode = map[title] if (episode != null) { val media1 = episode.media val media2 = e.media if (media1 != null && media2 != null && datesLookSimilar(episode, e) && durationsLookSimilar(media1, media2) && mimeTypeLooksSimilar(media1, media2)) { - Logd(TAG, "FeedAssistant init title duplicate: $title ${e.title}") - addDownloadStatus(e, episode) + Logd(TAG, "FeedAssistant init $tag title duplicate: $title ${e.title}") + if (savedFeedId > 0L) { + addDownloadStatus(e, episode) + iterator.remove() + } continue } } @@ -670,25 +526,21 @@ object Feeds { } } } - private fun addDownloadStatus(episode: Episode, possibleDuplicate: Episode) { - val feedId_ = if (feedId > 0) feedId else feed.id - addDownloadStatus(DownloadResult(feedId_, episode.title ?: "", DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false, + addDownloadStatus(DownloadResult(savedFeedId, episode.title ?: "", DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false, """ - The podcast host appears to have added the same episode twice. Podcini still refreshed the feed and attempted to repair it. - - Original episode: - ${EpisodeAssistant.duplicateEpisodeDetails(episode)} - - Second episode that is also in the feed: - ${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)} - """.trimIndent())) + The podcast host appears to have added the same episode twice. Podcini still refreshed the feed and attempted to repair it. + + Original episode: + ${EpisodeAssistant.duplicateEpisodeDetails(episode)} + + Second episode that is also in the feed: + ${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)} + """.trimIndent())) } - fun searchEpisodeByIdentifyingValue(item: Episode): Episode? { return map[item.identifyingValue] } - fun searchEpisodeGuessDuplicate(item: Episode): Episode? { var episode = map[item.identifier] if (episode != null) return episode @@ -709,7 +561,6 @@ object Feeds { } return null } - fun clear() { map.clear() } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/handler/EpisodeMenuHandler.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/handler/EpisodeMenuHandler.kt index a6ed9776..53b8c9b4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/handler/EpisodeMenuHandler.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/handler/EpisodeMenuHandler.kt @@ -5,7 +5,6 @@ import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink -import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch import ac.mdiq.podcini.playback.base.InTheatre import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curState @@ -145,7 +144,7 @@ object EpisodeMenuHandler { if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) { val media: EpisodeMedia? = selectedItem.media // not all items have media, Gpodder only cares about those that do - if (needSynch() && media != null) { + if (isProviderConnected && media != null) { val actionPlay: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY) .currentTimestamp() .started(media.getDuration() / 1000) @@ -159,7 +158,7 @@ object EpisodeMenuHandler { R.id.mark_unread_item -> { // selectedItem.setPlayed(false) setPlayState(Episode.PlayState.UNPLAYED.code, false, selectedItem) - if (needSynch() && selectedItem.feed?.isLocalFeed != true && selectedItem.media != null) { + if (isProviderConnected && selectedItem.feed?.isLocalFeed != true && selectedItem.media != null) { val actionNew: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.NEW) .currentTimestamp() .build() diff --git a/changelog.md b/changelog.md index 01219e6b..27700d88 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,8 @@ +# 6.7.1 + +* ensured duplicate episodes are removed from secondary checking during refresh +* refresh progress is updated in notification + # 6.7.0 * largely improved efficiency of podcasts refresh, no more massive list searches diff --git a/fastlane/metadata/android/en-US/changelogs/3020254.txt b/fastlane/metadata/android/en-US/changelogs/3020254.txt new file mode 100644 index 00000000..0a5de10c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020254.txt @@ -0,0 +1,4 @@ + Version 6.7.1: + +* ensured duplicate episodes are removed from secondary checking during refresh +* refresh progress is updated in notification