6.7.1 commit

This commit is contained in:
Xilin Jia 2024-09-18 20:13:40 +01:00
parent 692426d114
commit daeee36985
11 changed files with 77 additions and 227 deletions

View File

@ -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. #### 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) 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 #### 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. #### 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](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024. This project was developed from a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.

View File

@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests" testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020253 versionCode 3020254
versionName "6.7.0" versionName "6.7.1"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""

View File

@ -1,10 +1,9 @@
package ac.mdiq.podcini.net.download.service package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.net.utils.UrlChecker.prepareUrl 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.EpisodeMedia
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.showStackTrace
import android.os.Bundle import android.os.Bundle
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
@ -27,6 +26,7 @@ class DownloadRequest private constructor(
var size: Long = 0 var size: Long = 0
private var statusMsg = 0 private var statusMsg = 0
// only used in tests
constructor(destination: String, source: String, title: String, feedfileId: Long, constructor(destination: String, source: String, title: String, feedfileId: Long,
feedfileType: Int, username: String?, password: String?, arguments: Bundle?, initiatedByUser: Boolean) feedfileType: Int, username: String?, password: String?, arguments: Bundle?, initiatedByUser: Boolean)
: this(destination, source, title, feedfileId, feedfileType, null, username, password, false, arguments, initiatedByUser) : this(destination, source, title, feedfileId, feedfileType, null, username, password, false, arguments, initiatedByUser)

View File

@ -3,9 +3,9 @@ package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create 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.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink 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.net.utils.NetworkUtils.isAllowMobileEpisodeDownload
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs 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.storage.utils.MediaMetadataRetrieverCompat
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.utils.NotificationUtils 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.EventFlow
import ac.mdiq.podcini.util.FlowEvent 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.Notification
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
@ -328,7 +328,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item)) EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
// TODO: should use different event? // TODO: should use different event?
if (broadcastUnreadStateUpdate) EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(item)) if (broadcastUnreadStateUpdate) EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(item))
if (needSynch()) { if (isProviderConnected) {
Logd(TAG, "enqueue synch") Logd(TAG, "enqueue synch")
val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD).currentTimestamp().build() val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD).currentTimestamp().build()
SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action) SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action)

View File

@ -3,8 +3,8 @@ package ac.mdiq.podcini.net.feed
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.download.service.DefaultDownloaderFactory 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.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
import ac.mdiq.podcini.net.feed.parser.FeedHandler.FeedHandlerResult import ac.mdiq.podcini.net.feed.parser.FeedHandler.FeedHandlerResult
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh 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.media3.common.util.UnstableApi
import androidx.work.* import androidx.work.*
import androidx.work.Constraints.Builder 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.android.material.dialog.MaterialAlertDialogBuilder
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture 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 feedsToUpdate.shuffle() // If the worker gets cancelled early, every feed has a chance to be updated
} else { } else {
val feed = Feeds.getFeed(feedId) ?: return Result.success() 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 if (!feed.isLocalFeed) allAreLocal = false
feedsToUpdate = mutableListOf(feed) feedsToUpdate = mutableListOf(feed)
// feedsToUpdate.add(feed) // Needs to be updatable, so no singletonList // feedsToUpdate.add(feed) // Needs to be updatable, so no singletonList
@ -181,14 +179,13 @@ object FeedUpdateManager {
feedsToUpdate.clear() feedsToUpdate.clear()
return Result.success() return Result.success()
} }
private fun createNotification(toUpdate: List<Feed?>?): Notification { private fun createNotification(titles: List<String>?): Notification {
val context = applicationContext val context = applicationContext
var contentText = "" var contentText = ""
var bigText: String? = "" var bigText: String? = ""
if (toUpdate != null) { if (titles != null) {
contentText = context.resources.getQuantityString(R.plurals.downloads_left, contentText = context.resources.getQuantityString(R.plurals.downloads_left, titles.size, titles.size)
toUpdate.size, toUpdate.size) bigText = titles.map { "$it" }.joinToString("\n")
bigText = Stream.of(toUpdate).map { feed: Feed? -> "" + feed!!.title }.collect(Collectors.joining("\n"))
} }
return NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID.downloading.name) return NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID.downloading.name)
.setContentTitle(context.getString(R.string.download_notification_title_feeds)) .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)) .addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label), WorkManager.getInstance(context).createCancelPendingIntent(id))
.build() .build()
} }
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> { override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
return Futures.immediateFuture(ForegroundInfo(R.id.notification_updating_feeds, createNotification(null))) 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() // Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
return return
} }
val titles = feedsToUpdate.map { it.title ?: "No title" }.toMutableList()
var i = 0 var i = 0
while (i < feedsToUpdate.size) { while (i < feedsToUpdate.size) {
if (isStopped) return 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++]) val feed = unmanaged(feedsToUpdate[i++])
try { try {
Logd(TAG, "updating local feed? ${feed.isLocalFeed} ${feed.title}") 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?:"") val status = DownloadResult(feed.id, feed.title?:"", DownloadError.ERROR_IO_ERROR, false, e.message?:"")
LogsAndStats.addDownloadStatus(status) LogsAndStats.addDownloadStatus(status)
} }
// toUpdate.removeAt(0) titles.removeAt(0)
} }
} }
private fun refreshYoutubeFeed(feed: Feed) { private fun refreshYoutubeFeed(feed: Feed) {
@ -283,23 +282,23 @@ object FeedUpdateManager {
LogsAndStats.addDownloadStatus(downloader.result) LogsAndStats.addDownloadStatus(downloader.result)
return return
} }
val feedSyncTask = FeedSyncTask(applicationContext, request) val feedUpdateTask = FeedUpdateTask(applicationContext, request)
val success = feedSyncTask.run() val success = feedUpdateTask.run()
if (!success) { if (!success) {
Logd(TAG, "update failed: unsuccessful") Logd(TAG, "update failed: unsuccessful")
Feeds.persistFeedLastUpdateFailed(feed, true) Feeds.persistFeedLastUpdateFailed(feed, true)
LogsAndStats.addDownloadStatus(feedSyncTask.downloadStatus) LogsAndStats.addDownloadStatus(feedUpdateTask.downloadStatus)
return return
} }
if (request.feedfileId == null) return // No download logs for new subscriptions if (request.feedfileId == null) return // No download logs for new subscriptions
// we create a 'successful' download log if the feed's last refresh failed // we create a 'successful' download log if the feed's last refresh failed
val log = LogsAndStats.getFeedDownloadLog(request.feedfileId) 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()) { if (!request.source.isNullOrEmpty()) {
when { when {
!downloader.permanentRedirectUrl.isNullOrEmpty() -> Feeds.updateFeedDownloadURL(request.source, downloader.permanentRedirectUrl!!) !downloader.permanentRedirectUrl.isNullOrEmpty() -> Feeds.updateFeedDownloadURL(request.source, downloader.permanentRedirectUrl!!)
feedSyncTask.redirectUrl.isNotEmpty() && feedSyncTask.redirectUrl != request.source -> feedUpdateTask.redirectUrl.isNotEmpty() && feedUpdateTask.redirectUrl != request.source ->
Feeds.updateFeedDownloadURL(request.source, feedSyncTask.redirectUrl) 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 val task = FeedParserTask(request)
private var feedHandlerResult: FeedHandlerResult? = null private var feedHandlerResult: FeedHandlerResult? = null
val downloadStatus: DownloadResult val downloadStatus: DownloadResult

View File

@ -11,10 +11,6 @@ object SynchronizationQueueSink {
// To avoid a dependency loop of every class to SyncService, and from SyncService back to every class. // To avoid a dependency loop of every class to SyncService, and from SyncService back to every class.
private var serviceStarterImpl = Runnable {} private var serviceStarterImpl = Runnable {}
fun needSynch() : Boolean {
return isProviderConnected
}
fun setServiceStarterImpl(serviceStarter: Runnable) { fun setServiceStarterImpl(serviceStarter: Runnable) {
serviceStarterImpl = serviceStarter serviceStarterImpl = serviceStarter
} }
@ -34,7 +30,6 @@ object SynchronizationQueueSink {
fun enqueueFeedAddedIfSyncActive(context: Context, downloadUrl: String) { fun enqueueFeedAddedIfSyncActive(context: Context, downloadUrl: String) {
if (!isProviderConnected) return if (!isProviderConnected) return
LockingAsyncExecutor.executeLockedAsync { LockingAsyncExecutor.executeLockedAsync {
SynchronizationQueueStorage(context).enqueueFeedAdded(downloadUrl) SynchronizationQueueStorage(context).enqueueFeedAdded(downloadUrl)
syncNow() syncNow()
@ -43,7 +38,6 @@ object SynchronizationQueueSink {
fun enqueueFeedRemovedIfSyncActive(context: Context, downloadUrl: String) { fun enqueueFeedRemovedIfSyncActive(context: Context, downloadUrl: String) {
if (!isProviderConnected) return if (!isProviderConnected) return
LockingAsyncExecutor.executeLockedAsync { LockingAsyncExecutor.executeLockedAsync {
SynchronizationQueueStorage(context).enqueueFeedRemoved(downloadUrl) SynchronizationQueueStorage(context).enqueueFeedRemoved(downloadUrl)
syncNow() syncNow()
@ -52,7 +46,6 @@ object SynchronizationQueueSink {
fun enqueueEpisodeActionIfSyncActive(context: Context, action: EpisodeAction) { fun enqueueEpisodeActionIfSyncActive(context: Context, action: EpisodeAction) {
if (!isProviderConnected) return if (!isProviderConnected) return
LockingAsyncExecutor.executeLockedAsync { LockingAsyncExecutor.executeLockedAsync {
SynchronizationQueueStorage(context).enqueueEpisodeAction(action) SynchronizationQueueStorage(context).enqueueEpisodeAction(action)
syncNow() syncNow()
@ -61,7 +54,6 @@ object SynchronizationQueueSink {
fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) { fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) {
if (!isProviderConnected) return if (!isProviderConnected) return
val item_ = media.episodeOrFetch() val item_ = media.episodeOrFetch()
if (item_?.feed?.isLocalFeed == true) return if (item_?.feed?.isLocalFeed == true) return
if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return

View File

@ -3,9 +3,9 @@ package ac.mdiq.podcini.storage.database
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.updateFeed 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.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink 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.curQueue
import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying 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 // Do full update of this feed to get rid of the episode
if (episode.feed != null) updateFeed(episode.feed!!, context.applicationContext, null) if (episode.feed != null) updateFeed(episode.feed!!, context.applicationContext, null)
} else { } else {
if (needSynch()) { if (isProviderConnected) {
// Gpodder: queue delete action for synchronization // Gpodder: queue delete action for synchronization
val action = EpisodeAction.Builder(episode, EpisodeAction.DELETE).currentTimestamp().build() val action = EpisodeAction.Builder(episode, EpisodeAction.DELETE).currentTimestamp().build()
SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action) SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action)

View File

@ -1,9 +1,9 @@
package ac.mdiq.podcini.storage.database package ac.mdiq.podcini.storage.database
import ac.mdiq.podcini.net.download.DownloadError 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.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink 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.playback.base.VideoMode
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal 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. * 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. * @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<Episode> = 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 @Synchronized
fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? { fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? {
Logd(TAG, "updateFeed called") Logd(TAG, "updateFeed called")
@ -365,11 +229,6 @@ object Feeds {
Logd(TAG, "New feed has a higher page number.") Logd(TAG, "New feed has a higher page number.")
savedFeed.nextPageLink = newFeed.nextPageLink 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 priorMostRecent = savedFeed.mostRecentItem
val priorMostRecentDate: Date? = priorMostRecent?.getPubDate() val priorMostRecentDate: Date? = priorMostRecent?.getPubDate()
var idLong = Feed.newId() var idLong = Feed.newId()
@ -381,21 +240,6 @@ object Feeds {
// Look for new or updated Items // Look for new or updated Items
for (idx in newFeed.episodes.indices) { for (idx in newFeed.episodes.indices) {
val episode = newFeed.episodes[idx] 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) var oldItem = savedFeedAssistant.searchEpisodeByIdentifyingValue(episode)
if (!newFeed.isLocalFeed && oldItem == null) { if (!newFeed.isLocalFeed && oldItem == null) {
oldItem = savedFeedAssistant.searchEpisodeGuessDuplicate(episode) oldItem = savedFeedAssistant.searchEpisodeGuessDuplicate(episode)
@ -413,7 +257,7 @@ object Feeds {
${EpisodeAssistant.duplicateEpisodeDetails(episode)} ${EpisodeAssistant.duplicateEpisodeDetails(episode)}
""".trimIndent())) """.trimIndent()))
oldItem.identifier = episode.identifier 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 durs = oldItem.media!!.getDuration() / 1000
val action = EpisodeAction.Builder(oldItem, EpisodeAction.PLAY) val action = EpisodeAction.Builder(oldItem, EpisodeAction.PLAY)
.currentTimestamp() .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<String, Episode>() val map = mutableMapOf<String, Episode>()
val tag: String = if (savedFeedId == 0L) "Saved feed" else "New feed"
init { init {
for (e in feed.episodes) { val iterator = feed.episodes.iterator()
while (iterator.hasNext()) {
val e = iterator.next()
if (!e.identifier.isNullOrEmpty()) { if (!e.identifier.isNullOrEmpty()) {
if (map.containsKey(e.identifier!!)) { if (map.containsKey(e.identifier!!)) {
// TODO: add addDownloadStatus Logd(TAG, "FeedAssistant init $tag identifier duplicate: ${e.identifier} ${e.title}")
Logd(TAG, "FeedAssistant init identifier duplicate: ${e.identifier} ${e.title}") if (savedFeedId > 0L) {
addDownloadStatus(e, map[e.identifier!!]!!) addDownloadStatus(e, map[e.identifier!!]!!)
iterator.remove()
}
continue continue
} }
map[e.identifier!!] = e map[e.identifier!!] = e
@ -633,9 +483,11 @@ object Feeds {
val idv = e.identifyingValue val idv = e.identifyingValue
if (idv != e.identifier && !idv.isNullOrEmpty()) { if (idv != e.identifier && !idv.isNullOrEmpty()) {
if (map.containsKey(idv)) { if (map.containsKey(idv)) {
// TODO: add addDownloadStatus Logd(TAG, "FeedAssistant init $tag identifyingValue duplicate: $idv ${e.title}")
Logd(TAG, "FeedAssistant init identifyingValue duplicate: $idv ${e.title}") if (savedFeedId > 0L) {
addDownloadStatus(e, map[idv]!!) addDownloadStatus(e, map[idv]!!)
iterator.remove()
}
continue continue
} }
map[idv] = e map[idv] = e
@ -643,9 +495,11 @@ object Feeds {
val url = e.media?.getStreamUrl() val url = e.media?.getStreamUrl()
if (url != idv && !url.isNullOrEmpty()) { if (url != idv && !url.isNullOrEmpty()) {
if (map.containsKey(url)) { if (map.containsKey(url)) {
// TODO: add addDownloadStatus Logd(TAG, "FeedAssistant init $tag url duplicate: $url ${e.title}")
Logd(TAG, "FeedAssistant init url duplicate: $url ${e.title}") if (savedFeedId > 0L) {
addDownloadStatus(e, map[url]!!) addDownloadStatus(e, map[url]!!)
iterator.remove()
}
continue continue
} }
map[url] = e map[url] = e
@ -653,14 +507,16 @@ object Feeds {
val title = canonicalizeTitle(e.title) val title = canonicalizeTitle(e.title)
if (title != idv && title.isNotEmpty()) { if (title != idv && title.isNotEmpty()) {
if (map.containsKey(title)) { if (map.containsKey(title)) {
// TODO: add addDownloadStatus
val episode = map[title] val episode = map[title]
if (episode != null) { if (episode != null) {
val media1 = episode.media val media1 = episode.media
val media2 = e.media val media2 = e.media
if (media1 != null && media2 != null && datesLookSimilar(episode, e) && durationsLookSimilar(media1, media2) && mimeTypeLooksSimilar(media1, media2)) { if (media1 != null && media2 != null && datesLookSimilar(episode, e) && durationsLookSimilar(media1, media2) && mimeTypeLooksSimilar(media1, media2)) {
Logd(TAG, "FeedAssistant init title duplicate: $title ${e.title}") Logd(TAG, "FeedAssistant init $tag title duplicate: $title ${e.title}")
if (savedFeedId > 0L) {
addDownloadStatus(e, episode) addDownloadStatus(e, episode)
iterator.remove()
}
continue continue
} }
} }
@ -670,10 +526,8 @@ object Feeds {
} }
} }
} }
private fun addDownloadStatus(episode: Episode, possibleDuplicate: Episode) { private fun addDownloadStatus(episode: Episode, possibleDuplicate: Episode) {
val feedId_ = if (feedId > 0) feedId else feed.id addDownloadStatus(DownloadResult(savedFeedId, episode.title ?: "", DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false,
addDownloadStatus(DownloadResult(feedId_, 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. The podcast host appears to have added the same episode twice. Podcini still refreshed the feed and attempted to repair it.
@ -684,11 +538,9 @@ object Feeds {
${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)} ${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)}
""".trimIndent())) """.trimIndent()))
} }
fun searchEpisodeByIdentifyingValue(item: Episode): Episode? { fun searchEpisodeByIdentifyingValue(item: Episode): Episode? {
return map[item.identifyingValue] return map[item.identifyingValue]
} }
fun searchEpisodeGuessDuplicate(item: Episode): Episode? { fun searchEpisodeGuessDuplicate(item: Episode): Episode? {
var episode = map[item.identifier] var episode = map[item.identifier]
if (episode != null) return episode if (episode != null) return episode
@ -709,7 +561,6 @@ object Feeds {
} }
return null return null
} }
fun clear() { fun clear() {
map.clear() map.clear()
} }

View File

@ -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.SynchronizationSettings.wifiSyncEnabledKey
import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink 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
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.curState
@ -145,7 +144,7 @@ object EpisodeMenuHandler {
if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) { if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) {
val media: EpisodeMedia? = selectedItem.media val media: EpisodeMedia? = selectedItem.media
// not all items have media, Gpodder only cares about those that do // 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) val actionPlay: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.PLAY)
.currentTimestamp() .currentTimestamp()
.started(media.getDuration() / 1000) .started(media.getDuration() / 1000)
@ -159,7 +158,7 @@ object EpisodeMenuHandler {
R.id.mark_unread_item -> { R.id.mark_unread_item -> {
// selectedItem.setPlayed(false) // selectedItem.setPlayed(false)
setPlayState(Episode.PlayState.UNPLAYED.code, false, selectedItem) 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) val actionNew: EpisodeAction = EpisodeAction.Builder(selectedItem, EpisodeAction.NEW)
.currentTimestamp() .currentTimestamp()
.build() .build()

View File

@ -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 # 6.7.0
* largely improved efficiency of podcasts refresh, no more massive list searches * largely improved efficiency of podcasts refresh, no more massive list searches

View File

@ -0,0 +1,4 @@
Version 6.7.1:
* ensured duplicate episodes are removed from secondary checking during refresh
* refresh progress is updated in notification