Skip to content

Commit

Permalink
6.3.4 commit
Browse files Browse the repository at this point in the history
  • Loading branch information
XilinJia committed Aug 4, 2024
1 parent e5188bc commit 5fe3f04
Show file tree
Hide file tree
Showing 47 changed files with 382 additions and 284 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Compared to AntennaPod this project:
3. Iron-age celebrity SQLite is replaced with modern object-base Realm DB (Podcini.R),
4. Outfits with Viewbinding, Coil replacing Glide, coroutines replacing RxJava and threads, SharedFlow replacing EventBus, and jetifier removed,
5. Boasts new UI's including streamlined drawer, subscriptions view and player controller,
6. Supports multiple and circular play queues associable to any podcast
6. Supports multiple, virtual and circular play queues associable to any podcast
7. Auto-download is governed by policy and limit settings of individual feed
8. Accepts podcast as well as plain RSS and YouTube feeds,
9. Offers Readability and Text-to-Speech for RSS contents,
Expand Down Expand Up @@ -59,13 +59,20 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* easy switches on video player to other video mode or audio only
* default video player mode setting in preferences
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
* "Prefer streaming over download" is now on setting of individual feed
* Multiple queues can be used: 5 queues are provided by default, user can rename or add up to 10 queues
* on app startup, the most recently updated queue is set to curQueue
* any episodes can be easily added/moved to the active or any designated queues
* any queue can be associated with any feed for customized playing experience
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
* Every queue has a bin containing past episodes removed from the queue
* Episode played from a list other than the queue is now a one-off play, unless the episode is on the active queue, in which case, the next episode in the queue will be played
* Feed associated queue can be set to None, in which case:
* episodes in the feed are not automatically added to any queue, but are used as a natural queue for getting the next episode to play
* the next episode is determined in such a way:
* if the currently playing episode had been (manually) added to the active queue, then it's the next in queue
* else if "prefer streaming" is set, it's the next unplayed episode in the feed episodes list based on the current sort order
* else it's the next downloaded unplayed episode
* Otherwise, episode played from a list other than the queue is now a one-off play, unless the episode is on the active queue, in which case, the next episode in the queue will be played


### Podcast/Episode list
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

versionCode 3020227
versionName "6.3.3"
versionCode 3020228
versionName "6.3.4"

applicationId "ac.mdiq.podcini.R"
def commit = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class MediaPlayerBaseTest {
VolumeAdaptionSetting.OFF, null, null)
f.preferences = prefs
f.episodes.clear()
val i = Episode(0, "t", "i", "l", Date(), Episode.UNPLAYED, f)
val i = Episode(0, "t", "i", "l", Date(), Episode.PlayState.UNPLAYED.code, f)
f.episodes.add(i)
val media = EpisodeMedia(0, i, 0, 0, 0, "audio/wav", fileUrl, downloadUrl, fileUrl != null, null, 0, 0)
i.setMedia(media)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class TaskManagerTest {
val f = Feed(0, null, "title", "link", "d", null, null, null, null, "id", null, "null", "url")
f.episodes.clear()
for (i in 0 until NUM_ITEMS) {
f.episodes.add(Episode(0, pref + i, pref + i, "link", Date(), Episode.PLAYED, f))
f.episodes.add(Episode(0, pref + i, pref + i, "link", Date(), Episode.PlayState.PLAYED.code, f))
}
// val adapter = getInstance()
// adapter.open()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class UITestUtils(private val context: Context) {
val items: MutableList<Episode> = ArrayList()
for (j in 0 until NUM_ITEMS_PER_FEED) {
val item = Episode(j.toLong(), "Feed " + (i + 1) + ": Item " + (j + 1), "item$j",
"http://example.com/feed$i/item/$j", Date(), Episode.UNPLAYED, feed)
"http://example.com/feed$i/item/$j", Date(), Episode.PlayState.UNPLAYED.code, feed)
items.add(item)

if (!hostTextOnlyFeeds) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.LogsAndStats
import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.DownloadResult
import ac.mdiq.podcini.storage.model.Episode
Expand Down Expand Up @@ -78,7 +77,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Logd(TAG, "starting cancel")
// This needs to be done here, not in the worker. Reason: The worker might or might not be running.
val item_ = media.episodeOrFetch()
if (item_ != null) Episodes.deleteMediaOfEpisode(context, item_) // Remove partially downloaded file
if (item_ != null) Episodes.deleteEpisodeMedia(context, item_) // Remove partially downloaded file
val tag = WORK_TAG_EPISODE_URL + media.downloadUrl
val future: Future<List<WorkInfo>> = WorkManager.getInstance(context).getWorkInfosByTag(tag)

Expand Down Expand Up @@ -124,7 +123,8 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
.addTag(WORK_TAG)
.addTag(WORK_TAG_EPISODE_URL + item.media!!.downloadUrl)
if (enqueueDownloadedEpisodes()) {
runBlocking { Queues.addToQueueSync(false, item, item.feed?.preferences?.queue) }
if (item.feed?.preferences?.queue != null)
runBlocking { Queues.addToQueueSync(false, item, item.feed?.preferences?.queue) }
workRequest.addTag(WORK_DATA_WAS_QUEUED)
}
workRequest.setInputData(Data.Builder().putLong(WORK_DATA_MEDIA_ID, item.media!!.id).build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ object LocalFeedUpdater {
}

private fun createFeedItem(feed: Feed, file: FastDocumentFile, context: Context): Episode {
val item = Episode(0L, file.name, UUID.randomUUID().toString(), file.name, Date(file.lastModified), Episode.UNPLAYED, feed)
val item = Episode(0L, file.name, UUID.randomUUID().toString(), file.name, Date(file.lastModified), Episode.PlayState.UNPLAYED.code, feed)
item.disableAutoDownload()

val size = file.length
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,11 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
curMedia = playable
if (curMedia is EpisodeMedia) {
val media_ = curMedia as EpisodeMedia
curIndexInQueue = EpisodeUtil.indexOfItemWithId(curQueue.episodes, media_.id)
val item = media_.episodeOrFetch()
val eList = if (item?.feed?.preferences?.queue != null) curQueue.episodes else item?.feed?.getVirtualQueueItems() ?: listOf()
curIndexInQueue = EpisodeUtil.indexOfItemWithId(eList, media_.id)
} else curIndexInQueue = -1

prevMedia = curMedia
this.isStreaming = stream
mediaType = curMedia!!.getMediaType()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ class PlaybackService : MediaSessionService() {
if (ended || smartMarkAsPlayed || autoSkipped || (skipped && !shouldSkipKeepEpisode())) {
Logd(TAG, "onPostPlayback ended: $ended smartMarkAsPlayed: $smartMarkAsPlayed autoSkipped: $autoSkipped skipped: $skipped")
// only mark the item as played if we're not keeping it anyways
item = setPlayStateSync(Episode.PLAYED, ended || (skipped && smartMarkAsPlayed), item!!)
item = setPlayStateSync(Episode.PlayState.PLAYED.code, ended || (skipped && smartMarkAsPlayed), item!!)
val action = item?.feed?.preferences?.autoDeleteAction
val shouldAutoDelete = (action == AutoDeleteAction.ALWAYS ||
(action == AutoDeleteAction.GLOBAL && item?.feed != null && shouldAutoDeleteItem(item!!.feed!!)))
Expand Down Expand Up @@ -355,11 +355,6 @@ class PlaybackService : MediaSessionService() {

override fun getNextInQueue(currentMedia: Playable?): Playable? {
Logd(TAG, "call getNextInQueue currentMedia: ${currentMedia?.getEpisodeTitle()}")
if (curIndexInQueue < 0) {
Logd(TAG, "getNextInQueue(), curMedia is not in curQueue")
writeNoMediaPlaying()
return null
}
if (currentMedia !is EpisodeMedia) {
Logd(TAG, "getNextInQueue(), but playable not an instance of EpisodeMedia, so not proceeding")
writeNoMediaPlaying()
Expand All @@ -371,20 +366,29 @@ class PlaybackService : MediaSessionService() {
writeNoMediaPlaying()
return null
}
// val nextItem = getNextInQueue(item)
if (curQueue.episodes.isEmpty()) {
if (curIndexInQueue < 0 && item.feed?.preferences?.queue != null) {
Logd(TAG, "getNextInQueue(), curMedia is not in curQueue")
writeNoMediaPlaying()
return null
}
val eList = if (item.feed?.preferences?.queue == null) item.feed?.getVirtualQueueItems() else curQueue.episodes
if (eList.isNullOrEmpty()) {
Logd(TAG, "getNextInQueue queue is empty")
writeNoMediaPlaying()
return null
}
Logd(TAG, "getNextInQueue eList: ${eList.size}")
var j = 0
val i = EpisodeUtil.indexOfItemWithId(curQueue.episodes, item.id)
val i = EpisodeUtil.indexOfItemWithId(eList, item.id)
Logd(TAG, "getNextInQueue current i: $i curIndexInQueue: $curIndexInQueue")
if (i < 0) {
if (curIndexInQueue < curQueue.episodes.size) j = curIndexInQueue
else j = curQueue.episodes.size-1
} else if (i < curQueue.episodes.size-1) j = i+1
if (curIndexInQueue >= 0 && curIndexInQueue < eList.size) j = curIndexInQueue
else j = eList.size-1
} else if (i < eList.size-1) j = i+1
Logd(TAG, "getNextInQueue next j: $j")

val nextItem = unmanaged(curQueue.episodes[j])
val nextItem = unmanaged(eList[j])
Logd(TAG, "getNextInQueue nextItem ${nextItem.title}")
if (nextItem.media == null) {
Logd(TAG, "getNextInQueue nextItem: $nextItem media is null")
writeNoMediaPlaying()
Expand All @@ -397,15 +401,15 @@ class PlaybackService : MediaSessionService() {
return null
}

if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) {
if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed?.isLocalFeed != true) {
Logd(TAG, "getNextInQueue nextItem has no local file ${nextItem.title}")
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this@PlaybackService, nextItem.media!!).intent)
writeNoMediaPlaying()
return null
}
EventFlow.postEvent(FlowEvent.PlayEvent(item, FlowEvent.PlayEvent.Action.END))
EventFlow.postEvent(FlowEvent.PlayEvent(nextItem))
return if (nextItem.media == null) nextItem.media else unmanaged(nextItem.media!!)
return if (nextItem.media == null) null else unmanaged(nextItem.media!!)
}

override fun findMedia(url: String): Playable? {
Expand All @@ -419,14 +423,12 @@ class PlaybackService : MediaSessionService() {
if (stopPlaying) taskManager.cancelPositionSaver()

if (mediaType == null) sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0)
else {
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
when {
isCasting -> EXTRA_CODE_CAST
mediaType == MediaType.VIDEO -> EXTRA_CODE_VIDEO
else -> EXTRA_CODE_AUDIO
})
}
else sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
when {
isCasting -> EXTRA_CODE_CAST
mediaType == MediaType.VIDEO -> EXTRA_CODE_VIDEO
else -> EXTRA_CODE_AUDIO
})
}

override fun ensureMediaInfoLoaded(media: Playable) {
Expand Down Expand Up @@ -962,7 +964,7 @@ class PlaybackService : MediaSessionService() {
if (event.action == FlowEvent.QueueEvent.Action.REMOVED) {
Logd(TAG, "onQueueEvent: ending playback curEpisode ${curEpisode?.title}")
for (e in event.episodes) {
Logd(TAG, "onQueueEvent: ending playback event ${e?.title}")
Logd(TAG, "onQueueEvent: ending playback event ${e.title}")
if (e.id == curEpisode?.id) {
mPlayer?.endPlayback(hasEnded = false, wasSkipped = true, shouldContinue = true, toStoppedState = true)
break
Expand Down Expand Up @@ -1075,7 +1077,7 @@ class PlaybackService : MediaSessionService() {
if (media != null) {
media.setPosition(position)
media.setLastPlayedTime(System.currentTimeMillis())
if (it.isNew) it.playState = Episode.UNPLAYED
if (it.isNew) it.playState = Episode.PlayState.UNPLAYED.code
if (media.startPosition >= 0 && media.getPosition() > media.startPosition)
media.playedDuration = (media.playedDurationWhenStarted + media.getPosition() - media.startPosition)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.EPISODE_CLEANUP_NULL
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodeMedia
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
import ac.mdiq.podcini.storage.database.Queues.getInQueueEpisodeIds
Expand Down Expand Up @@ -84,7 +84,7 @@ object AutoCleanups {
for (item in delete) {
if (item.media == null) continue
try {
runBlocking { deleteMediaOfEpisode(context, item).join() }
runBlocking { deleteEpisodeMedia(context, item).join() }
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (e: ExecutionException) {
Expand Down Expand Up @@ -138,7 +138,7 @@ object AutoCleanups {
for (item in delete) {
if (item.media == null) continue
try {
runBlocking { deleteMediaOfEpisode(context, item).join() }
runBlocking { deleteEpisodeMedia(context, item).join() }
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (e: ExecutionException) {
Expand Down Expand Up @@ -205,7 +205,7 @@ object AutoCleanups {
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
for (item in delete) {
try {
runBlocking { deleteMediaOfEpisode(context, item).join() }
runBlocking { deleteEpisodeMedia(context, item).join() }
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (e: ExecutionException) {
Expand Down
20 changes: 9 additions & 11 deletions app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Episode.Companion.BUILDING
import ac.mdiq.podcini.storage.model.Episode.Companion.NEW
import ac.mdiq.podcini.storage.model.Episode.Companion.PLAYED
import ac.mdiq.podcini.storage.model.Episode.Companion.UNPLAYED
import ac.mdiq.podcini.storage.model.Episode.PlayState
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
Expand Down Expand Up @@ -98,7 +95,7 @@ object Episodes {

// @JvmStatic is needed because some Runnable blocks call this
@OptIn(UnstableApi::class) @JvmStatic
fun deleteMediaOfEpisode(context: Context, episode: Episode) : Job {
fun deleteEpisodeMedia(context: Context, episode: Episode) : Job {
Logd(TAG, "deleteMediaOfEpisode called ${episode.title}")
return runOnIOScope {
if (episode.media == null) return@runOnIOScope
Expand Down Expand Up @@ -136,7 +133,7 @@ object Episodes {
url != null -> {
// delete downloaded media file
val mediaFile = File(url)
if (mediaFile.exists() && !mediaFile.delete()) {
if (!mediaFile.delete()) {
Log.e(TAG, "delete media file failed: $url")
val evt = FlowEvent.MessageEvent(context.getString(R.string.delete_failed))
EventFlow.postEvent(evt)
Expand Down Expand Up @@ -176,6 +173,7 @@ object Episodes {
* Remove the listed episodes and their EpisodeMedia entries.
* Deleting media also removes the download log entries.
*/
@UnstableApi
fun deleteEpisodes(context: Context, episodes: List<Episode>) : Job {
return runOnIOScope {
val removedFromQueue: MutableList<Episode> = ArrayList()
Expand Down Expand Up @@ -290,19 +288,19 @@ object Episodes {
suspend fun setPlayStateSync(played: Int, resetMediaPosition: Boolean, episode: Episode) : Episode {
Logd(TAG, "setPlayStateSync called resetMediaPosition: $resetMediaPosition")
val result = upsert(episode) {
if (played >= NEW && played <= BUILDING) it.playState = played
if (played >= PlayState.NEW.code && played <= PlayState.BUILDING.code) it.playState = played
else {
if (it.playState == PLAYED) it.playState = UNPLAYED
else it.playState = PLAYED
if (it.playState == PlayState.PLAYED.code) it.playState = PlayState.UNPLAYED.code
else it.playState = PlayState.PLAYED.code
}
if (resetMediaPosition) it.media?.setPosition(0)
}
if (played == PLAYED && shouldRemoveFromQueuesMarkPlayed()) removeFromAllQueuesSync(result)
if (played == PlayState.PLAYED.code && shouldMarkedPlayedRemoveFromQueues()) removeFromAllQueuesSync(result)
EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(result))
return result
}

private fun shouldRemoveFromQueuesMarkPlayed(): Boolean {
private fun shouldMarkedPlayedRemoveFromQueues(): Boolean {
return appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true)
}
}
7 changes: 0 additions & 7 deletions app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt
Original file line number Diff line number Diff line change
Expand Up @@ -377,13 +377,6 @@ object Feeds {
backupManager.dataChanged()
}

// private fun persistFeedsSync(vararg feeds: Feed) {
// Logd(TAG, "persistFeedsSync called")
// for (feed in feeds) {
// upsertBlk(feed) {}
// }
// }

fun persistFeedPreferences(feed: Feed) : Job {
Logd(TAG, "persistFeedPreferences called")
return runOnIOScope {
Expand Down
Loading

0 comments on commit 5fe3f04

Please sign in to comment.