6.7.0 commit
This commit is contained in:
parent
338788a8ad
commit
692426d114
|
@ -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 3020252
|
versionCode 3020253
|
||||||
versionName "6.6.7"
|
versionName "6.7.0"
|
||||||
|
|
||||||
applicationId "ac.mdiq.podcini.R"
|
applicationId "ac.mdiq.podcini.R"
|
||||||
def commit = ""
|
def commit = ""
|
||||||
|
|
|
@ -3,6 +3,8 @@ 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.Feed
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
|
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
|
||||||
|
@ -120,6 +122,8 @@ class DownloadRequest private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLastModified(lastModified: String?): DownloadRequest {
|
fun setLastModified(lastModified: String?): DownloadRequest {
|
||||||
|
Logd("DownloadRequest", "setLastModified: $lastModified")
|
||||||
|
// showStackTrace()
|
||||||
this.lastModified = lastModified
|
this.lastModified = lastModified
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -143,7 +147,6 @@ class DownloadRequest private constructor(
|
||||||
this.feedfileId = media.id
|
this.feedfileId = media.id
|
||||||
this.feedfileType = media.getTypeAsInt()
|
this.feedfileType = media.getTypeAsInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(destination: String, feed: Feed) {
|
constructor(destination: String, feed: Feed) {
|
||||||
this.destination = destination
|
this.destination = destination
|
||||||
this.source = when {
|
this.source = when {
|
||||||
|
@ -156,27 +159,22 @@ class DownloadRequest private constructor(
|
||||||
this.feedfileType = feed.getTypeAsInt()
|
this.feedfileType = feed.getTypeAsInt()
|
||||||
arguments.putInt(REQUEST_ARG_PAGE_NR, feed.pageNr)
|
arguments.putInt(REQUEST_ARG_PAGE_NR, feed.pageNr)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withInitiatedByUser(initiatedByUser: Boolean): Builder {
|
fun withInitiatedByUser(initiatedByUser: Boolean): Builder {
|
||||||
this.initiatedByUser = initiatedByUser
|
this.initiatedByUser = initiatedByUser
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setForce(force: Boolean) {
|
fun setForce(force: Boolean) {
|
||||||
if (force) lastModified = null
|
if (force) lastModified = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun lastModified(lastModified: String?): Builder {
|
fun lastModified(lastModified: String?): Builder {
|
||||||
this.lastModified = lastModified
|
this.lastModified = lastModified
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withAuthentication(username: String?, password: String?): Builder {
|
fun withAuthentication(username: String?, password: String?): Builder {
|
||||||
this.username = username
|
this.username = username
|
||||||
this.password = password
|
this.password = password
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(): DownloadRequest {
|
fun build(): DownloadRequest {
|
||||||
return DownloadRequest(this)
|
return DownloadRequest(this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,8 +134,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
||||||
}
|
}
|
||||||
progressUpdaterThread.start()
|
progressUpdaterThread.start()
|
||||||
var result: Result
|
var result: Result
|
||||||
try {
|
try { result = performDownload(media, request)
|
||||||
result = performDownload(media, request)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
result = Result.failure()
|
result = Result.failure()
|
||||||
|
@ -170,32 +169,23 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
||||||
}
|
}
|
||||||
val dest = File(request.destination)
|
val dest = File(request.destination)
|
||||||
if (!dest.exists()) {
|
if (!dest.exists()) {
|
||||||
try {
|
try { dest.createNewFile() } catch (e: IOException) { Log.e(TAG, "performDownload Unable to create file") }
|
||||||
dest.createNewFile()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(TAG, "performDownload Unable to create file")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (dest.exists()) {
|
if (dest.exists()) {
|
||||||
try {
|
try {
|
||||||
var episode = realm.query(Episode::class).query("id == ${media.id}").first().find()
|
var episode = realm.query(Episode::class).query("id == ${media.id}").first().find()
|
||||||
if (episode != null) {
|
if (episode != null) {
|
||||||
episode = upsertBlk(episode) {
|
episode = upsertBlk(episode) { it.media?.setfileUrlOrNull(request.destination) }
|
||||||
it.media?.setfileUrlOrNull(request.destination)
|
|
||||||
}
|
|
||||||
EventFlow.postEvent(FlowEvent.EpisodeMediaEvent.updated(episode))
|
EventFlow.postEvent(FlowEvent.EpisodeMediaEvent.updated(episode))
|
||||||
} else Log.e(TAG, "performDownload media.episode is null")
|
} else Log.e(TAG, "performDownload media.episode is null")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) { Log.e(TAG, "performDownload Exception in writeFileUrl: " + e.message) }
|
||||||
Log.e(TAG, "performDownload Exception in writeFileUrl: " + e.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
downloader = DefaultDownloaderFactory().create(request)
|
downloader = DefaultDownloaderFactory().create(request)
|
||||||
if (downloader == null) {
|
if (downloader == null) {
|
||||||
Log.e(TAG, "performDownload Unable to create downloader")
|
Log.e(TAG, "performDownload Unable to create downloader")
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
try {
|
try { downloader!!.call()
|
||||||
downloader!!.call()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "failed performDownload exception on downloader!!.call() ${e.message}")
|
Log.e(TAG, "failed performDownload exception on downloader!!.call() ${e.message}")
|
||||||
LogsAndStats.addDownloadStatus(downloader!!.result)
|
LogsAndStats.addDownloadStatus(downloader!!.result)
|
||||||
|
@ -328,8 +318,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
||||||
durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||||
if (durationStr != null) it.media?.setDuration(durationStr!!.toInt())
|
if (durationStr != null) it.media?.setDuration(durationStr!!.toInt())
|
||||||
}
|
}
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) { Logd(TAG, "Invalid file duration: $durationStr")
|
||||||
Logd(TAG, "Invalid file duration: $durationStr")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Get duration failed", e)
|
Log.e(TAG, "Get duration failed", e)
|
||||||
it.media?.setDuration(30000)
|
it.media?.setDuration(30000)
|
||||||
|
|
|
@ -195,8 +195,7 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun newCall(httpReq: Request.Builder): Response {
|
private fun newCall(httpReq: Request.Builder): Response {
|
||||||
var httpClient = getHttpClient()
|
var httpClient = getHttpClient()
|
||||||
try {
|
try { return httpClient.newCall(httpReq.build()).execute()
|
||||||
return httpClient.newCall(httpReq.build()).execute()
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, e.toString())
|
Log.e(TAG, e.toString())
|
||||||
if (e.message != null && e.message!!.contains("PROTOCOL_ERROR")) {
|
if (e.message != null && e.message!!.contains("PROTOCOL_ERROR")) {
|
||||||
|
|
|
@ -148,25 +148,25 @@ object FeedUpdateManager {
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
ClientConfigurator.initialize(applicationContext)
|
ClientConfigurator.initialize(applicationContext)
|
||||||
val toUpdate: MutableList<Feed>
|
val feedsToUpdate: MutableList<Feed>
|
||||||
val feedId = inputData.getLong(EXTRA_FEED_ID, -1L)
|
val feedId = inputData.getLong(EXTRA_FEED_ID, -1L)
|
||||||
var allAreLocal = true
|
var allAreLocal = true
|
||||||
var force = false
|
var force = false
|
||||||
if (feedId == -1L) { // Update all
|
if (feedId == -1L) { // Update all
|
||||||
toUpdate = Feeds.getFeedList().toMutableList()
|
feedsToUpdate = Feeds.getFeedList().toMutableList()
|
||||||
val itr = toUpdate.iterator()
|
val itr = feedsToUpdate.iterator()
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
val feed = itr.next()
|
val feed = itr.next()
|
||||||
if (feed.preferences?.keepUpdated == false) itr.remove()
|
if (feed.preferences?.keepUpdated == false) itr.remove()
|
||||||
if (!feed.isLocalFeed) allAreLocal = false
|
if (!feed.isLocalFeed) allAreLocal = false
|
||||||
}
|
}
|
||||||
toUpdate.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 feed.downloadUrl: ${feed.downloadUrl}")
|
||||||
if (!feed.isLocalFeed) allAreLocal = false
|
if (!feed.isLocalFeed) allAreLocal = false
|
||||||
toUpdate = ArrayList()
|
feedsToUpdate = mutableListOf(feed)
|
||||||
toUpdate.add(feed) // Needs to be updatable, so no singletonList
|
// feedsToUpdate.add(feed) // Needs to be updatable, so no singletonList
|
||||||
force = true
|
force = true
|
||||||
}
|
}
|
||||||
if (!inputData.getBoolean(EXTRA_EVEN_ON_MOBILE, false) && !allAreLocal) {
|
if (!inputData.getBoolean(EXTRA_EVEN_ON_MOBILE, false) && !allAreLocal) {
|
||||||
|
@ -175,10 +175,10 @@ object FeedUpdateManager {
|
||||||
return Result.retry()
|
return Result.retry()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshFeeds(toUpdate, force)
|
refreshFeeds(feedsToUpdate, force)
|
||||||
notificationManager.cancel(R.id.notification_updating_feeds)
|
notificationManager.cancel(R.id.notification_updating_feeds)
|
||||||
autodownloadEpisodeMedia(applicationContext, toUpdate.toList())
|
autodownloadEpisodeMedia(applicationContext, feedsToUpdate.toList())
|
||||||
toUpdate.clear()
|
feedsToUpdate.clear()
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
private fun createNotification(toUpdate: List<Feed?>?): Notification {
|
private fun createNotification(toUpdate: List<Feed?>?): Notification {
|
||||||
|
@ -203,7 +203,7 @@ object FeedUpdateManager {
|
||||||
return Futures.immediateFuture(ForegroundInfo(R.id.notification_updating_feeds, createNotification(null)))
|
return Futures.immediateFuture(ForegroundInfo(R.id.notification_updating_feeds, createNotification(null)))
|
||||||
}
|
}
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
private fun refreshFeeds(toUpdate: MutableList<Feed>, force: Boolean) {
|
private fun refreshFeeds(feedsToUpdate: MutableList<Feed>, force: Boolean) {
|
||||||
if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this.applicationContext,
|
if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this.applicationContext,
|
||||||
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
// TODO: Consider calling
|
// TODO: Consider calling
|
||||||
|
@ -219,10 +219,10 @@ object FeedUpdateManager {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var i = 0
|
var i = 0
|
||||||
while (i < toUpdate.size) {
|
while (i < feedsToUpdate.size) {
|
||||||
if (isStopped) return
|
if (isStopped) return
|
||||||
notificationManager.notify(R.id.notification_updating_feeds, createNotification(toUpdate))
|
notificationManager.notify(R.id.notification_updating_feeds, createNotification(feedsToUpdate))
|
||||||
val feed = unmanaged(toUpdate[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}")
|
||||||
when {
|
when {
|
||||||
|
|
|
@ -2097,15 +2097,8 @@ class PlaybackService : MediaLibraryService() {
|
||||||
private var positionSaverFuture: ScheduledFuture<*>? = null
|
private var positionSaverFuture: ScheduledFuture<*>? = null
|
||||||
private var widgetUpdaterFuture: ScheduledFuture<*>? = null
|
private var widgetUpdaterFuture: ScheduledFuture<*>? = null
|
||||||
private var sleepTimerFuture: ScheduledFuture<*>? = null
|
private var sleepTimerFuture: ScheduledFuture<*>? = null
|
||||||
|
|
||||||
// @Volatile
|
|
||||||
// private var chapterLoaderFuture: Disposable? = null
|
|
||||||
|
|
||||||
private var sleepTimer: SleepTimer? = null
|
private var sleepTimer: SleepTimer? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the sleep timer is currently active.
|
|
||||||
*/
|
|
||||||
@get:Synchronized
|
@get:Synchronized
|
||||||
val isSleepTimerActive: Boolean
|
val isSleepTimerActive: Boolean
|
||||||
get() = sleepTimerFuture?.isCancelled == false && sleepTimerFuture?.isDone == false && (sleepTimer?.getWaitingTime() ?: 0) > 0
|
get() = sleepTimerFuture?.isCancelled == false && sleepTimerFuture?.isDone == false && (sleepTimer?.getWaitingTime() ?: 0) > 0
|
||||||
|
@ -2124,16 +2117,10 @@ class PlaybackService : MediaLibraryService() {
|
||||||
val isWidgetUpdaterActive: Boolean
|
val isWidgetUpdaterActive: Boolean
|
||||||
get() = widgetUpdaterFuture != null && !widgetUpdaterFuture!!.isCancelled && !widgetUpdaterFuture!!.isDone
|
get() = widgetUpdaterFuture != null && !widgetUpdaterFuture!!.isCancelled && !widgetUpdaterFuture!!.isDone
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the position saver is currently running.
|
|
||||||
*/
|
|
||||||
@get:Synchronized
|
@get:Synchronized
|
||||||
val isPositionSaverActive: Boolean
|
val isPositionSaverActive: Boolean
|
||||||
get() = positionSaverFuture != null && !positionSaverFuture!!.isCancelled && !positionSaverFuture!!.isDone
|
get() = positionSaverFuture != null && !positionSaverFuture!!.isCancelled && !positionSaverFuture!!.isDone
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the position saver task. If the position saver is already active, nothing will happen.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun startPositionSaver() {
|
fun startPositionSaver() {
|
||||||
if (!isPositionSaverActive) {
|
if (!isPositionSaverActive) {
|
||||||
|
@ -2145,9 +2132,6 @@ class PlaybackService : MediaLibraryService() {
|
||||||
} else Logd(TAG, "Call to startPositionSaver was ignored.")
|
} else Logd(TAG, "Call to startPositionSaver was ignored.")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels the position saver. If the position saver is not running, nothing will happen.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun cancelPositionSaver() {
|
fun cancelPositionSaver() {
|
||||||
if (isPositionSaverActive) {
|
if (isPositionSaverActive) {
|
||||||
|
@ -2156,9 +2140,6 @@ class PlaybackService : MediaLibraryService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the widget updater task. If the widget updater is already active, nothing will happen.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun startWidgetUpdater() {
|
fun startWidgetUpdater() {
|
||||||
if (!isWidgetUpdaterActive && !schedExecutor.isShutdown) {
|
if (!isWidgetUpdaterActive && !schedExecutor.isShutdown) {
|
||||||
|
@ -2184,13 +2165,11 @@ class PlaybackService : MediaLibraryService() {
|
||||||
* Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be
|
* Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be
|
||||||
* cancelled first.
|
* cancelled first.
|
||||||
* After waitingTime has elapsed, onSleepTimerExpired() will be called.
|
* After waitingTime has elapsed, onSleepTimerExpired() will be called.
|
||||||
*
|
|
||||||
* @throws java.lang.IllegalArgumentException if waitingTime <= 0
|
* @throws java.lang.IllegalArgumentException if waitingTime <= 0
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun setSleepTimer(waitingTime: Long) {
|
fun setSleepTimer(waitingTime: Long) {
|
||||||
require(waitingTime > 0) { "Waiting time <= 0" }
|
require(waitingTime > 0) { "Waiting time <= 0" }
|
||||||
|
|
||||||
Logd(TAG, "Setting sleep timer to $waitingTime milliseconds")
|
Logd(TAG, "Setting sleep timer to $waitingTime milliseconds")
|
||||||
if (isSleepTimerActive) sleepTimerFuture!!.cancel(true)
|
if (isSleepTimerActive) sleepTimerFuture!!.cancel(true)
|
||||||
sleepTimer = SleepTimer(waitingTime)
|
sleepTimer = SleepTimer(waitingTime)
|
||||||
|
@ -2198,9 +2177,6 @@ class PlaybackService : MediaLibraryService() {
|
||||||
EventFlow.postEvent(FlowEvent.SleepTimerUpdatedEvent.justEnabled(waitingTime))
|
EventFlow.postEvent(FlowEvent.SleepTimerUpdatedEvent.justEnabled(waitingTime))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables the sleep timer. If the sleep timer is not active, nothing will happen.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun disableSleepTimer() {
|
fun disableSleepTimer() {
|
||||||
if (isSleepTimerActive) {
|
if (isSleepTimerActive) {
|
||||||
|
@ -2209,9 +2185,6 @@ class PlaybackService : MediaLibraryService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Restarts the sleep timer. If the sleep timer is not active, nothing will happen.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun restartSleepTimer() {
|
fun restartSleepTimer() {
|
||||||
if (isSleepTimerActive) {
|
if (isSleepTimerActive) {
|
||||||
|
@ -2353,8 +2326,7 @@ class PlaybackService : MediaLibraryService() {
|
||||||
fun onChapterLoaded(media: Playable?)
|
fun onChapterLoaded(media: Playable?)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class ShakeListener(private val mContext: Context, private val mSleepTimer: SleepTimer) :
|
internal class ShakeListener(private val mContext: Context, private val mSleepTimer: SleepTimer) : SensorEventListener {
|
||||||
SensorEventListener {
|
|
||||||
private var mAccelerometer: Sensor? = null
|
private var mAccelerometer: Sensor? = null
|
||||||
private var mSensorMgr: SensorManager? = null
|
private var mSensorMgr: SensorManager? = null
|
||||||
|
|
||||||
|
@ -2389,14 +2361,10 @@ class PlaybackService : MediaLibraryService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
|
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
|
||||||
companion object {
|
|
||||||
private val TAG: String = ShakeListener::class.simpleName ?: "Anonymous"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = TaskManager::class.simpleName ?: "Anonymous"
|
private val TAG: String = TaskManager::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
private const val SCHED_EX_POOL_SIZE = 2
|
private const val SCHED_EX_POOL_SIZE = 2
|
||||||
|
|
||||||
private const val SLEEP_TIMER_UPDATE_INTERVAL = 10000L // in millisoconds
|
private const val SLEEP_TIMER_UPDATE_INTERVAL = 10000L // in millisoconds
|
||||||
|
|
|
@ -9,6 +9,10 @@ import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal
|
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
|
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.EpisodeAssistant.searchEpisodeByIdentifyingValue
|
import ac.mdiq.podcini.storage.database.Feeds.EpisodeAssistant.searchEpisodeByIdentifyingValue
|
||||||
|
import ac.mdiq.podcini.storage.database.Feeds.EpisodeDuplicateGuesser.canonicalizeTitle
|
||||||
|
import ac.mdiq.podcini.storage.database.Feeds.EpisodeDuplicateGuesser.datesLookSimilar
|
||||||
|
import ac.mdiq.podcini.storage.database.Feeds.EpisodeDuplicateGuesser.durationsLookSimilar
|
||||||
|
import ac.mdiq.podcini.storage.database.Feeds.EpisodeDuplicateGuesser.mimeTypeLooksSimilar
|
||||||
import ac.mdiq.podcini.storage.database.LogsAndStats.addDownloadStatus
|
import ac.mdiq.podcini.storage.database.LogsAndStats.addDownloadStatus
|
||||||
import ac.mdiq.podcini.storage.database.Queues.addToQueueSync
|
import ac.mdiq.podcini.storage.database.Queues.addToQueueSync
|
||||||
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet
|
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet
|
||||||
|
@ -192,6 +196,142 @@ 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")
|
||||||
|
@ -233,27 +373,32 @@ object Feeds {
|
||||||
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()
|
||||||
|
Logd(TAG, "updateFeed building newFeedAssistant")
|
||||||
|
val newFeedAssistant = FeedAssistant(newFeed, savedFeed.id)
|
||||||
|
Logd(TAG, "updateFeed building savedFeedAssistant")
|
||||||
|
val savedFeedAssistant = FeedAssistant(savedFeed)
|
||||||
|
|
||||||
// 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)
|
// val possibleDuplicate = EpisodeAssistant.searchEpisodeGuessDuplicate(newFeed.episodes, episode)
|
||||||
if (!newFeed.isLocalFeed && possibleDuplicate != null && episode !== possibleDuplicate) {
|
// if (!newFeed.isLocalFeed && possibleDuplicate != null && episode !== possibleDuplicate) {
|
||||||
// Canonical episode is the first one returned (usually oldest)
|
// // Canonical episode is the first one returned (usually oldest)
|
||||||
addDownloadStatus(DownloadResult(savedFeed.id, episode.title ?: "", DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, false,
|
// 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.
|
// The podcast host appears to have added the same episode twice. Podcini still refreshed the feed and attempted to repair it.
|
||||||
|
//
|
||||||
Original episode:
|
// Original episode:
|
||||||
${EpisodeAssistant.duplicateEpisodeDetails(episode)}
|
// ${EpisodeAssistant.duplicateEpisodeDetails(episode)}
|
||||||
|
//
|
||||||
Second episode that is also in the feed:
|
// Second episode that is also in the feed:
|
||||||
${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)}
|
// ${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)}
|
||||||
""".trimIndent()))
|
// """.trimIndent()))
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
var oldItem = searchEpisodeByIdentifyingValue(savedFeed.episodes, episode)
|
var oldItem = savedFeedAssistant.searchEpisodeByIdentifyingValue(episode)
|
||||||
if (!newFeed.isLocalFeed && oldItem == null) {
|
if (!newFeed.isLocalFeed && oldItem == null) {
|
||||||
oldItem = EpisodeAssistant.searchEpisodeGuessDuplicate(savedFeed.episodes, episode)
|
oldItem = savedFeedAssistant.searchEpisodeGuessDuplicate(episode)
|
||||||
if (oldItem != null) {
|
if (oldItem != null) {
|
||||||
Logd(TAG, "Repaired duplicate: $oldItem, $episode")
|
Logd(TAG, "Repaired duplicate: $oldItem, $episode")
|
||||||
addDownloadStatus(DownloadResult(savedFeed.id,
|
addDownloadStatus(DownloadResult(savedFeed.id,
|
||||||
|
@ -304,17 +449,21 @@ object Feeds {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
savedFeedAssistant.clear()
|
||||||
|
|
||||||
// identify episodes to be removed
|
// identify episodes to be removed
|
||||||
if (removeUnlistedItems) {
|
if (removeUnlistedItems) {
|
||||||
val it = savedFeed.episodes.toMutableList().iterator()
|
val it = savedFeed.episodes.toMutableList().iterator()
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
val feedItem = it.next()
|
val feedItem = it.next()
|
||||||
if (searchEpisodeByIdentifyingValue(newFeed.episodes, feedItem) == null) {
|
if (newFeedAssistant.searchEpisodeByIdentifyingValue(feedItem) == null) {
|
||||||
unlistedItems.add(feedItem)
|
unlistedItems.add(feedItem)
|
||||||
it.remove()
|
it.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
newFeedAssistant.clear()
|
||||||
|
|
||||||
// update attributes
|
// update attributes
|
||||||
savedFeed.lastUpdate = newFeed.lastUpdate
|
savedFeed.lastUpdate = newFeed.lastUpdate
|
||||||
savedFeed.type = newFeed.type
|
savedFeed.type = newFeed.type
|
||||||
|
@ -421,7 +570,7 @@ object Feeds {
|
||||||
return !feed.isLocalFeed || isAutoDeleteLocal
|
return !feed.isLocalFeed || isAutoDeleteLocal
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getYoutubeSyndicate(video: Boolean, music: Boolean): Feed {
|
private fun getYoutubeSyndicate(video: Boolean, music: Boolean): Feed {
|
||||||
var feedId: Long = if (video) 1 else 2
|
var feedId: Long = if (video) 1 else 2
|
||||||
if (music) feedId += 2 // music feed takes ids 3 and 4
|
if (music) feedId += 2 // music feed takes ids 3 and 4
|
||||||
var feed = getFeed(feedId, true)
|
var feed = getFeed(feedId, true)
|
||||||
|
@ -467,6 +616,104 @@ object Feeds {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FeedAssistant(val feed: Feed, val feedId: Long = 0L) {
|
||||||
|
val map = mutableMapOf<String, Episode>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
for (e in feed.episodes) {
|
||||||
|
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!!]!!)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
map[e.identifier!!] = e
|
||||||
|
}
|
||||||
|
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]!!)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
map[idv] = e
|
||||||
|
}
|
||||||
|
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]!!)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
map[url] = e
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: does it mean there are duplicate titles?
|
||||||
|
map[title] = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
val url = item.media?.getStreamUrl()
|
||||||
|
if (!url.isNullOrEmpty()) {
|
||||||
|
episode = map[url]
|
||||||
|
if (episode != null) return episode
|
||||||
|
}
|
||||||
|
val title = canonicalizeTitle(item.title)
|
||||||
|
if (title.isNotEmpty()) {
|
||||||
|
episode = map[title]
|
||||||
|
if (episode != null) {
|
||||||
|
val media1 = episode.media
|
||||||
|
val media2 = item.media
|
||||||
|
if (media1 == null || media2 == null) return null
|
||||||
|
if (datesLookSimilar(episode, item) && durationsLookSimilar(media1, media2) && mimeTypeLooksSimilar(media1, media2)) return episode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
map.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
private object EpisodeAssistant {
|
private object EpisodeAssistant {
|
||||||
fun searchEpisodeByIdentifyingValue(episodes: List<Episode>?, searchItem: Episode): Episode? {
|
fun searchEpisodeByIdentifyingValue(episodes: List<Episode>?, searchItem: Episode): Episode? {
|
||||||
if (episodes.isNullOrEmpty()) return null
|
if (episodes.isNullOrEmpty()) return null
|
||||||
|
@ -479,16 +726,16 @@ object Feeds {
|
||||||
* Guess if one of the episodes could actually mean the searched episode, even if it uses another identifying value.
|
* Guess if one of the episodes could actually mean the searched episode, even if it uses another identifying value.
|
||||||
* This is to work around podcasters breaking their GUIDs.
|
* This is to work around podcasters breaking their GUIDs.
|
||||||
*/
|
*/
|
||||||
fun searchEpisodeGuessDuplicate(episodes: List<Episode>?, searchItem: Episode): Episode? {
|
// fun searchEpisodeGuessDuplicate(episodes: List<Episode>?, searchItem: Episode): Episode? {
|
||||||
if (episodes.isNullOrEmpty()) return null
|
// if (episodes.isNullOrEmpty()) return null
|
||||||
for (episode in episodes) {
|
// for (episode in episodes) {
|
||||||
if (EpisodeDuplicateGuesser.sameAndNotEmpty(episode.identifier, searchItem.identifier)) return episode
|
// if (EpisodeDuplicateGuesser.sameAndNotEmpty(episode.identifier, searchItem.identifier)) return episode
|
||||||
}
|
// }
|
||||||
for (episode in episodes) {
|
// for (episode in episodes) {
|
||||||
if (EpisodeDuplicateGuesser.seemDuplicates(episode, searchItem)) return episode
|
// if (EpisodeDuplicateGuesser.seemDuplicates(episode, searchItem)) return episode
|
||||||
}
|
// }
|
||||||
return null
|
// return null
|
||||||
}
|
// }
|
||||||
fun duplicateEpisodeDetails(episode: Episode): String {
|
fun duplicateEpisodeDetails(episode: Episode): String {
|
||||||
return ("""
|
return ("""
|
||||||
Title: ${episode.title}
|
Title: ${episode.title}
|
||||||
|
@ -506,6 +753,7 @@ object Feeds {
|
||||||
* even if their feed explicitly says that the episodes are different.
|
* even if their feed explicitly says that the episodes are different.
|
||||||
*/
|
*/
|
||||||
object EpisodeDuplicateGuesser {
|
object EpisodeDuplicateGuesser {
|
||||||
|
// only used in test
|
||||||
fun seemDuplicates(item1: Episode, item2: Episode): Boolean {
|
fun seemDuplicates(item1: Episode, item2: Episode): Boolean {
|
||||||
if (sameAndNotEmpty(item1.identifier, item2.identifier)) return true
|
if (sameAndNotEmpty(item1.identifier, item2.identifier)) return true
|
||||||
val media1 = item1.media
|
val media1 = item1.media
|
||||||
|
@ -514,21 +762,21 @@ object Feeds {
|
||||||
if (sameAndNotEmpty(media1.getStreamUrl(), media2.getStreamUrl())) return true
|
if (sameAndNotEmpty(media1.getStreamUrl(), media2.getStreamUrl())) return true
|
||||||
return (titlesLookSimilar(item1, item2) && datesLookSimilar(item1, item2) && durationsLookSimilar(media1, media2) && mimeTypeLooksSimilar(media1, media2))
|
return (titlesLookSimilar(item1, item2) && datesLookSimilar(item1, item2) && durationsLookSimilar(media1, media2) && mimeTypeLooksSimilar(media1, media2))
|
||||||
}
|
}
|
||||||
fun sameAndNotEmpty(string1: String?, string2: String?): Boolean {
|
private fun sameAndNotEmpty(string1: String?, string2: String?): Boolean {
|
||||||
if (string1.isNullOrEmpty() || string2.isNullOrEmpty()) return false
|
if (string1.isNullOrEmpty() || string2.isNullOrEmpty()) return false
|
||||||
return string1 == string2
|
return string1 == string2
|
||||||
}
|
}
|
||||||
private fun datesLookSimilar(item1: Episode, item2: Episode): Boolean {
|
internal fun datesLookSimilar(item1: Episode, item2: Episode): Boolean {
|
||||||
if (item1.getPubDate() == null || item2.getPubDate() == null) return false
|
if (item1.getPubDate() == null || item2.getPubDate() == null) return false
|
||||||
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US) // MM/DD/YY
|
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US) // MM/DD/YY
|
||||||
val dateOriginal = dateFormat.format(item2.getPubDate()!!)
|
val dateOriginal = dateFormat.format(item2.getPubDate()!!)
|
||||||
val dateNew = dateFormat.format(item1.getPubDate()!!)
|
val dateNew = dateFormat.format(item1.getPubDate()!!)
|
||||||
return dateOriginal == dateNew // Same date; time is ignored.
|
return dateOriginal == dateNew // Same date; time is ignored.
|
||||||
}
|
}
|
||||||
private fun durationsLookSimilar(media1: EpisodeMedia, media2: EpisodeMedia): Boolean {
|
internal fun durationsLookSimilar(media1: EpisodeMedia, media2: EpisodeMedia): Boolean {
|
||||||
return abs((media1.getDuration() - media2.getDuration()).toDouble()) < 10 * 60L * 1000L
|
return abs((media1.getDuration() - media2.getDuration()).toDouble()) < 10 * 60L * 1000L
|
||||||
}
|
}
|
||||||
private fun mimeTypeLooksSimilar(media1: EpisodeMedia, media2: EpisodeMedia): Boolean {
|
internal fun mimeTypeLooksSimilar(media1: EpisodeMedia, media2: EpisodeMedia): Boolean {
|
||||||
var mimeType1 = media1.mimeType
|
var mimeType1 = media1.mimeType
|
||||||
var mimeType2 = media2.mimeType
|
var mimeType2 = media2.mimeType
|
||||||
if (mimeType1 == null || mimeType2 == null) return true
|
if (mimeType1 == null || mimeType2 == null) return true
|
||||||
|
@ -541,7 +789,7 @@ object Feeds {
|
||||||
private fun titlesLookSimilar(item1: Episode, item2: Episode): Boolean {
|
private fun titlesLookSimilar(item1: Episode, item2: Episode): Boolean {
|
||||||
return sameAndNotEmpty(canonicalizeTitle(item1.title), canonicalizeTitle(item2.title))
|
return sameAndNotEmpty(canonicalizeTitle(item1.title), canonicalizeTitle(item2.title))
|
||||||
}
|
}
|
||||||
private fun canonicalizeTitle(title: String?): String {
|
internal fun canonicalizeTitle(title: String?): String {
|
||||||
if (title == null) return ""
|
if (title == null) return ""
|
||||||
return title
|
return title
|
||||||
.trim { it <= ' ' }
|
.trim { it <= ' ' }
|
||||||
|
|
|
@ -4,11 +4,17 @@ enum class MediaType {
|
||||||
AUDIO, VIDEO, UNKNOWN;
|
AUDIO, VIDEO, UNKNOWN;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val AUDIO_APPLICATION_MIME_STRINGS: Set<String> = HashSet(mutableListOf(
|
// private val AUDIO_APPLICATION_MIME_STRINGS: Set<String> = HashSet(mutableListOf(
|
||||||
|
// "application/ogg",
|
||||||
|
// "application/opus",
|
||||||
|
// "application/x-flac"
|
||||||
|
// ))
|
||||||
|
|
||||||
|
private val AUDIO_APPLICATION_MIME_STRINGS: HashSet<String> = hashSetOf(
|
||||||
"application/ogg",
|
"application/ogg",
|
||||||
"application/opus",
|
"application/opus",
|
||||||
"application/x-flac"
|
"application/x-flac"
|
||||||
))
|
)
|
||||||
|
|
||||||
fun fromMimeType(mimeType: String?): MediaType {
|
fun fromMimeType(mimeType: String?): MediaType {
|
||||||
return when {
|
return when {
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
# 6.7.0
|
||||||
|
|
||||||
|
* largely improved efficiency of podcasts refresh, no more massive list searches
|
||||||
|
|
||||||
# 6.6.7
|
# 6.6.7
|
||||||
|
|
||||||
* volume adaptation numbers were changed to 0.2, 0.5, 1, 1.6, 2.4, 3.6 to avoid much distortion
|
* volume adaptation numbers were changed to 0.2, 0.5, 1, 1.6, 2.4, 3.6 to avoid much distortion
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Version 6.7.0:
|
||||||
|
|
||||||
|
* largely improved efficiency of podcasts refresh, no more massive list searches
|
Loading…
Reference in New Issue