Skip to content

Commit 5a40c6a

Browse files
committed
6.2.2 commit
1 parent 13870ce commit 5a40c6a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+321
-211
lines changed

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
3434

3535
## Notable new features & enhancements
3636

37-
### Player
37+
### Player and Queues
3838

3939
* More convenient player control displayed on all pages
4040
* Revamped and more efficient expanded player view showing episode description on the front
@@ -59,6 +59,14 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
5959
* easy switches on video player to other video mode or audio only
6060
* default video player mode setting in preferences
6161
* 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
62+
* Multiple queues can be used: 5 queues are provided by default, user can rename or add up to 10 queues
63+
* on app startup, the most recently updated queue is set to curQueue
64+
* any episodes can be easily added/moved to the active or any designated queues
65+
* any queue can be associated with any feed for customized playing experience
66+
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
67+
* Every queue has a bin containing past episodes removed from the queue
68+
* 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
69+
6270

6371
### Podcast/Episode list
6472

@@ -82,11 +90,6 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
8290
* on action bar of FeedEpisodes view there is a direct access to Queue
8391
* Long-press filter button in FeedEpisodes view enables/disables filters without changing filter settings
8492
* History view shows time of last play, and allows filters and sorts
85-
* Multiple queues can be used: 5 queues are provided by default, user can add up to 10 queues
86-
* on app startup, the most recently updated queue is set to curQueue
87-
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
88-
* Every queue has a bin containing past episodes removed from the queue
89-
9093
### Podcast/Episode
9194

9295
* New share notes menu option on various episode views
@@ -121,11 +124,12 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
121124
* Each feed also has its own download policy (only new episodes, newest episodes, and oldest episodes. "newest episodes" meaning most recent episodes, new or old)
122125
* Each feed has its own limit (Episode cache) for number of episodes downloaded, this limit rules in combination of the overall limit for the app.
123126
* Auto downloads run feeds or feed refreshes, scheduled or manual
124-
* auto download always includes any undownloaded episodes (regardless of feeds) added in the current queue
127+
* auto download always includes any undownloaded episodes (regardless of feeds) added in the Default queue
125128
* After auto download run, episodes with New status is changed to Unplayed.
126129
* auto download feed setting dialog is also changed:
127130
* there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently
128131
* on exclusive dialog, there are optional check boxes "Exclude episodes shorter than" and "Mark excluded episodes played"
132+
* Sleep timer has a new option of "To the end of episode"
129133

130134
### Security and reliability
131135

app/build.gradle

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,14 @@ android {
2525
kotlinOptions {
2626
jvmTarget = '17'
2727
}
28-
composeOptions {
29-
kotlinCompilerExtensionVersion = "1.5.14"
30-
}
3128
vectorDrawables.useSupportLibrary false
3229
vectorDrawables.generatedDensities = []
3330

3431
testApplicationId "ac.mdiq.podcini.tests"
3532
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
3633

37-
versionCode 3020222
38-
versionName "6.2.1"
34+
versionCode 3020223
35+
versionName "6.2.2"
3936

4037
applicationId "ac.mdiq.podcini.R"
4138
def commit = ""

app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ object DownloadRequestCreator {
4444

4545
Logd(TAG, "Requesting download media from url " + media.downloadUrl)
4646

47-
val feed = media.episode?.feed
47+
val feed = media.episodeOrFetch()?.feed
4848
val username = feed?.preferences?.username
4949
val password = feed?.preferences?.password
5050

app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,19 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
7575
@OptIn(UnstableApi::class) override fun cancel(context: Context, media: EpisodeMedia) {
7676
Logd(TAG, "starting cancel")
7777
// This needs to be done here, not in the worker. Reason: The worker might or might not be running.
78-
if (media.episode != null) Episodes.deleteMediaOfEpisode(context, media.episode!!) // Remove partially downloaded file
78+
val item_ = media.episodeOrFetch()
79+
if (item_ != null) Episodes.deleteMediaOfEpisode(context, item_) // Remove partially downloaded file
7980
val tag = WORK_TAG_EPISODE_URL + media.downloadUrl
8081
val future: Future<List<WorkInfo>> = WorkManager.getInstance(context).getWorkInfosByTag(tag)
8182

8283
CoroutineScope(Dispatchers.IO).launch {
8384
try {
8485
val workInfoList = future.get() // Wait for the completion of the future operation and retrieve the result
8586
workInfoList.forEach { workInfo ->
87+
// TODO: why cancel so many times??
8688
if (workInfo.tags.contains(WORK_DATA_WAS_QUEUED)) {
87-
if (media.episode != null) Queues.removeFromQueue(media.episode!!)
89+
val item_ = media.episodeOrFetch()
90+
if (item_ != null) Queues.removeFromQueue(item_)
8891
}
8992
}
9093
WorkManager.getInstance(context).cancelAllWorkByTag(tag)
@@ -202,6 +205,10 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
202205
@OptIn(UnstableApi::class)
203206
private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result {
204207
Logd(TAG, "starting performDownload")
208+
if (request.destination == null) {
209+
Log.e(TAG, "performDownload request.destination is null")
210+
return Result.failure()
211+
}
205212
val dest = File(request.destination)
206213
if (!dest.exists()) {
207214
try {
@@ -338,17 +345,19 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
338345
return
339346
}
340347
// media.setDownloaded modifies played state
341-
val broadcastUnreadStateUpdate = media.episode != null && media.episode!!.isNew
348+
var item_ = media.episodeOrFetch()
349+
val broadcastUnreadStateUpdate = item_?.isNew == true
342350
// media.downloaded = true
343351
media.setIsDownloaded()
344-
Logd(TAG, "media.episode.isNew: ${media.episode?.isNew} ${media.episode?.playState}")
352+
item_ = media.episodeOrFetch()
353+
Logd(TAG, "media.episode.isNew: ${item_?.isNew} ${item_?.playState}")
345354
media.setfileUrlOrNull(request.destination)
346355
if (request.destination != null) media.size = File(request.destination).length()
347356
media.checkEmbeddedPicture() // enforce check
348357
// check if file has chapters
349-
if (media.episode != null && media.episode!!.chapters.isEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
350-
if (media.episode?.podcastIndexChapterUrl != null)
351-
ChapterUtils.loadChaptersFromUrl(media.episode!!.podcastIndexChapterUrl!!, false)
358+
if (item_?.chapters.isNullOrEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
359+
if (item_?.podcastIndexChapterUrl != null)
360+
ChapterUtils.loadChaptersFromUrl(item_.podcastIndexChapterUrl!!, false)
352361
// Get duration
353362
var durationStr: String? = null
354363
try {
@@ -364,7 +373,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
364373
Log.e(TAG, "Get duration failed", e)
365374
media.setDuration(30000)
366375
}
367-
val item = media.episode
376+
val item = media.episodeOrFetch()
368377
item?.media = media
369378
try {
370379
// we've received the media, we don't want to autodownload it again

app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueSink.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ object SynchronizationQueueSink {
6262
fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) {
6363
if (!isProviderConnected) return
6464

65-
if (media.episode?.feed == null || media.episode!!.feed!!.isLocalFeed) return
65+
val item_ = media.episodeOrFetch()
66+
if (item_?.feed?.isLocalFeed == true) return
6667
if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return
67-
val action = EpisodeAction.Builder(media.episode!!, EpisodeAction.PLAY)
68+
val action = EpisodeAction.Builder(item_!!, EpisodeAction.PLAY)
6869
.currentTimestamp()
6970
.started(media.startPosition / 1000)
7071
.position((if (completed) media.getDuration() else media.getPosition()) / 1000)

app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class PlaybackServiceStarter(private val context: Context, private val media: Pl
4848
if (media is EpisodeMedia) {
4949
curMedia = media
5050
// curEpisode = if (media.episode != null) unmanaged(media.episode!!) else null
51-
curEpisode = media.episode
51+
curEpisode = media.episodeOrFetch()
5252
// curMedia = curEpisode?.media
5353
} else curMedia = media
5454

app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,23 @@ import kotlinx.coroutines.*
1919
object InTheatre {
2020
val TAG: String = InTheatre::class.simpleName ?: "Anonymous"
2121

22+
var curIndexInQueue = -1
23+
2224
var curQueue: PlayQueue // unmanaged
2325

2426
var curEpisode: Episode? = null // unmanged
2527
set(value) {
26-
field = value
27-
if (curMedia != field?.media) curMedia = field?.media
28+
field = if (value != null) unmanaged(value) else null
29+
if (field?.media != null && curMedia?.getIdentifier() != field?.media?.getIdentifier()) curMedia = unmanaged(field!!.media!!)
2830
}
2931

3032
var curMedia: Playable? = null // unmanged if EpisodeMedia
3133
set(value) {
32-
field = if (value != null && value is EpisodeMedia) unmanaged(value) else value
33-
if (field is EpisodeMedia) {
34-
val media = (field as EpisodeMedia)
35-
if (curEpisode != media.episode) curEpisode = media.episode
34+
if (value is EpisodeMedia) {
35+
field = unmanaged(value)
36+
if (value.episode != null && curEpisode?.id != value.episode?.id) curEpisode = unmanaged(value.episode!!)
37+
} else {
38+
field = value
3639
}
3740
}
3841

@@ -115,7 +118,7 @@ object InTheatre {
115118
val mediaId = curState.curMediaId
116119
if (mediaId != 0L) {
117120
curMedia = getEpisodeMedia(mediaId)
118-
if (curEpisode != null) curEpisode = (curMedia as EpisodeMedia).episode
121+
if (curEpisode != null) curEpisode = (curMedia as EpisodeMedia).episodeOrFetch()
119122
}
120123
} else Log.e(TAG, "Could not restore Playable object from preferences")
121124
}

app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,8 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
346346
if (media != null) {
347347
playbackSpeed = curState.curTempSpeed
348348
if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL && media is EpisodeMedia) {
349-
if (media.episode?.feed?.preferences != null) playbackSpeed = media.episode!!.feed!!.preferences!!.playSpeed
349+
val prefs_ = media.episodeOrFetch()?.feed?.preferences
350+
if (prefs_ != null) playbackSpeed = prefs_.playSpeed
350351
}
351352
}
352353
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = getPlaybackSpeed(mediaType)

app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder
66
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
77
import ac.mdiq.podcini.net.utils.NetworkUtils.wasDownloadBlocked
88
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
9+
import ac.mdiq.podcini.playback.base.InTheatre.curIndexInQueue
910
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
11+
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
1012
import ac.mdiq.podcini.playback.base.MediaPlayerBase
1113
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
1214
import ac.mdiq.podcini.playback.base.PlayerStatus
@@ -15,11 +17,13 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
1517
import ac.mdiq.podcini.storage.model.EpisodeMedia
1618
import ac.mdiq.podcini.storage.model.Playable
1719
import ac.mdiq.podcini.storage.model.MediaType
20+
import ac.mdiq.podcini.storage.utils.EpisodeUtil
1821
import ac.mdiq.podcini.util.Logd
1922
import ac.mdiq.podcini.util.config.ClientConfig
2023
import ac.mdiq.podcini.util.event.EventFlow
2124
import ac.mdiq.podcini.util.event.FlowEvent
2225
import ac.mdiq.podcini.util.event.FlowEvent.PlayEvent.Action
26+
import ac.mdiq.podcini.util.showStackTrace
2327
import android.app.UiModeManager
2428
import android.content.Context
2529
import android.content.res.Configuration
@@ -225,11 +229,18 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
225229
*/
226230
override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean, forceReset: Boolean) {
227231
Logd(TAG, "playMediaObject status=$status stream=$stream startWhenPrepared=$startWhenPrepared prepareImmediately=$prepareImmediately forceReset=$forceReset ${playable.getEpisodeTitle()} ")
232+
// showStackTrace()
228233
if (curMedia != null) {
234+
Logd(TAG, "playMediaObject: curMedia exist status=$status")
229235
if (!forceReset && curMedia!!.getIdentifier() == prevMedia?.getIdentifier() && status == PlayerStatus.PLAYING) {
230236
Logd(TAG, "Method call to playMediaObject was ignored: media file already playing.")
231237
return
232238
}
239+
if (curMedia is EpisodeMedia) {
240+
val media_ = curMedia as EpisodeMedia
241+
curIndexInQueue = EpisodeUtil.indexOfItemWithId(curQueue.episodes, media_.id)
242+
} else curIndexInQueue = -1
243+
233244
Logd(TAG, "playMediaObject starts new media playable:${playable.getIdentifier()} curMedia:${curMedia!!.getIdentifier()} prevMedia:${prevMedia?.getIdentifier()}")
234245
// set temporarily to pause in order to update list with current position
235246
if (status == PlayerStatus.PLAYING) {
@@ -241,12 +252,12 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
241252
if (status == PlayerStatus.PAUSED || status == PlayerStatus.PLAYING || status == PlayerStatus.PREPARED) exoPlayer?.stop()
242253
// if (prevMedia != null && curMedia!!.getIdentifier() != prevMedia?.getIdentifier())
243254
// callback.onPostPlayback(prevMedia, ended = false, skipped = false, true)
244-
prevMedia = curMedia
245255
setPlayerStatus(PlayerStatus.INDETERMINATE, null)
246256
}
247257

248258
Logd(TAG, "playMediaObject preparing for playable:${playable.getIdentifier()} ${playable.getEpisodeTitle()}")
249259
curMedia = playable
260+
prevMedia = curMedia
250261
this.isStreaming = stream
251262
mediaType = curMedia!!.getMediaType()
252263
videoSize = null
@@ -264,7 +275,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
264275
if (streamurl != null) {
265276
val media = curMedia
266277
if (media is EpisodeMedia) {
267-
val preferences = media.episode?.feed?.preferences
278+
val preferences = media.episodeOrFetch()?.feed?.preferences
268279
setDataSource(metadata, streamurl, preferences?.username, preferences?.password)
269280
} else setDataSource(metadata, streamurl, null, null)
270281
}
@@ -289,6 +300,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
289300
e.printStackTrace()
290301
setPlayerStatus(PlayerStatus.ERROR, null)
291302
EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(e.localizedMessage ?: ""))
303+
} finally {
292304
}
293305
}
294306

@@ -431,13 +443,15 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
431443
var volumeLeft = volumeLeft
432444
var volumeRight = volumeRight
433445
val playable = curMedia
434-
if (playable is EpisodeMedia && playable.episode?.feed?.preferences != null) {
435-
val preferences = playable.episode!!.feed!!.preferences!!
436-
val volumeAdaptionSetting = preferences.volumeAdaptionSetting
437-
if (volumeAdaptionSetting != null) {
438-
val adaptionFactor = volumeAdaptionSetting.adaptionFactor
439-
volumeLeft *= adaptionFactor
440-
volumeRight *= adaptionFactor
446+
if (playable is EpisodeMedia) {
447+
val preferences = playable.episodeOrFetch()?.feed?.preferences
448+
if (preferences != null) {
449+
val volumeAdaptionSetting = preferences.volumeAdaptionSetting
450+
if (volumeAdaptionSetting != null) {
451+
val adaptionFactor = volumeAdaptionSetting.adaptionFactor
452+
volumeLeft *= adaptionFactor
453+
volumeRight *= adaptionFactor
454+
}
441455
}
442456
}
443457
if (volumeLeft > 1) {
@@ -532,6 +546,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
532546

533547
override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean) {
534548
releaseWifiLockIfNecessary()
549+
if (curMedia == null) return
535550

536551
val isPlaying = status == PlayerStatus.PLAYING
537552
// we're relying on the position stored in the Playable object for post-playback processing
@@ -566,7 +581,8 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
566581
else Logd(TAG, "Ignored call to stop: Current player state is: $status")
567582
}
568583
val hasNext = nextMedia != null
569-
callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext)
584+
if (currentMedia != null) callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext)
585+
// curMedia = nextMedia
570586
}
571587
isPlaying -> callback.onPlaybackPause(currentMedia, currentMedia!!.getPosition())
572588
}

0 commit comments

Comments
 (0)