6.7.1 commit
This commit is contained in:
parent
692426d114
commit
daeee36985
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Version 6.7.1:
|
||||
|
||||
* ensured duplicate episodes are removed from secondary checking during refresh
|
||||
* refresh progress is updated in notification
|
Loading…
Reference in New Issue