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.
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](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.

View File

@ -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 = ""

View File

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

View File

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

View File

@ -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<Feed?>?): Notification {
private fun createNotification(titles: List<String>?): 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<ForegroundInfo> {
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

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.
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

View File

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

View File

@ -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<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
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<String, Episode>()
val tag: String = if (savedFeedId == 0L) "Saved feed" else "New feed"
init {
for (e in feed.episodes) {
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}")
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}")
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}")
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}")
Logd(TAG, "FeedAssistant init $tag title duplicate: $title ${e.title}")
if (savedFeedId > 0L) {
addDownloadStatus(e, episode)
iterator.remove()
}
continue
}
}
@ -670,10 +526,8 @@ 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.
@ -684,11 +538,9 @@ object Feeds {
${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()
}

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.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()

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
* 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