Skip to content

Commit

Permalink
6.2.2 commit
Browse files Browse the repository at this point in the history
  • Loading branch information
XilinJia committed Jul 29, 2024
1 parent 13870ce commit 5a40c6a
Show file tree
Hide file tree
Showing 49 changed files with 321 additions and 211 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c

## Notable new features & enhancements

### Player
### Player and Queues

* More convenient player control displayed on all pages
* Revamped and more efficient expanded player view showing episode description on the front
Expand All @@ -59,6 +59,14 @@ 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
* 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


### Podcast/Episode list

Expand All @@ -82,11 +90,6 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* on action bar of FeedEpisodes view there is a direct access to Queue
* Long-press filter button in FeedEpisodes view enables/disables filters without changing filter settings
* History view shows time of last play, and allows filters and sorts
* Multiple queues can be used: 5 queues are provided by default, user can add up to 10 queues
* on app startup, the most recently updated queue is set to curQueue
* 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

### Podcast/Episode

* New share notes menu option on various episode views
Expand Down Expand Up @@ -121,11 +124,12 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* 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)
* 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.
* Auto downloads run feeds or feed refreshes, scheduled or manual
* auto download always includes any undownloaded episodes (regardless of feeds) added in the current queue
* auto download always includes any undownloaded episodes (regardless of feeds) added in the Default queue
* After auto download run, episodes with New status is changed to Unplayed.
* auto download feed setting dialog is also changed:
* there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently
* on exclusive dialog, there are optional check boxes "Exclude episodes shorter than" and "Mark excluded episodes played"
* Sleep timer has a new option of "To the end of episode"

### Security and reliability

Expand Down
7 changes: 2 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,14 @@ android {
kotlinOptions {
jvmTarget = '17'
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.14"
}
vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = []

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

versionCode 3020222
versionName "6.2.1"
versionCode 3020223
versionName "6.2.2"

applicationId "ac.mdiq.podcini.R"
def commit = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ object DownloadRequestCreator {

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

val feed = media.episode?.feed
val feed = media.episodeOrFetch()?.feed
val username = feed?.preferences?.username
val password = feed?.preferences?.password

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,19 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
@OptIn(UnstableApi::class) override fun cancel(context: Context, media: EpisodeMedia) {
Logd(TAG, "starting cancel")
// This needs to be done here, not in the worker. Reason: The worker might or might not be running.
if (media.episode != null) Episodes.deleteMediaOfEpisode(context, media.episode!!) // Remove partially downloaded file
val item_ = media.episodeOrFetch()
if (item_ != null) Episodes.deleteMediaOfEpisode(context, item_) // Remove partially downloaded file
val tag = WORK_TAG_EPISODE_URL + media.downloadUrl
val future: Future<List<WorkInfo>> = WorkManager.getInstance(context).getWorkInfosByTag(tag)

CoroutineScope(Dispatchers.IO).launch {
try {
val workInfoList = future.get() // Wait for the completion of the future operation and retrieve the result
workInfoList.forEach { workInfo ->
// TODO: why cancel so many times??
if (workInfo.tags.contains(WORK_DATA_WAS_QUEUED)) {
if (media.episode != null) Queues.removeFromQueue(media.episode!!)
val item_ = media.episodeOrFetch()
if (item_ != null) Queues.removeFromQueue(item_)
}
}
WorkManager.getInstance(context).cancelAllWorkByTag(tag)
Expand Down Expand Up @@ -202,6 +205,10 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
@OptIn(UnstableApi::class)
private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result {
Logd(TAG, "starting performDownload")
if (request.destination == null) {
Log.e(TAG, "performDownload request.destination is null")
return Result.failure()
}
val dest = File(request.destination)
if (!dest.exists()) {
try {
Expand Down Expand Up @@ -338,17 +345,19 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
return
}
// media.setDownloaded modifies played state
val broadcastUnreadStateUpdate = media.episode != null && media.episode!!.isNew
var item_ = media.episodeOrFetch()
val broadcastUnreadStateUpdate = item_?.isNew == true
// media.downloaded = true
media.setIsDownloaded()
Logd(TAG, "media.episode.isNew: ${media.episode?.isNew} ${media.episode?.playState}")
item_ = media.episodeOrFetch()
Logd(TAG, "media.episode.isNew: ${item_?.isNew} ${item_?.playState}")
media.setfileUrlOrNull(request.destination)
if (request.destination != null) media.size = File(request.destination).length()
media.checkEmbeddedPicture() // enforce check
// check if file has chapters
if (media.episode != null && media.episode!!.chapters.isEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
if (media.episode?.podcastIndexChapterUrl != null)
ChapterUtils.loadChaptersFromUrl(media.episode!!.podcastIndexChapterUrl!!, false)
if (item_?.chapters.isNullOrEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
if (item_?.podcastIndexChapterUrl != null)
ChapterUtils.loadChaptersFromUrl(item_.podcastIndexChapterUrl!!, false)
// Get duration
var durationStr: String? = null
try {
Expand All @@ -364,7 +373,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "Get duration failed", e)
media.setDuration(30000)
}
val item = media.episode
val item = media.episodeOrFetch()
item?.media = media
try {
// we've received the media, we don't want to autodownload it again
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ object SynchronizationQueueSink {
fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) {
if (!isProviderConnected) return

if (media.episode?.feed == null || media.episode!!.feed!!.isLocalFeed) return
val item_ = media.episodeOrFetch()
if (item_?.feed?.isLocalFeed == true) return
if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return
val action = EpisodeAction.Builder(media.episode!!, EpisodeAction.PLAY)
val action = EpisodeAction.Builder(item_!!, EpisodeAction.PLAY)
.currentTimestamp()
.started(media.startPosition / 1000)
.position((if (completed) media.getDuration() else media.getPosition()) / 1000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class PlaybackServiceStarter(private val context: Context, private val media: Pl
if (media is EpisodeMedia) {
curMedia = media
// curEpisode = if (media.episode != null) unmanaged(media.episode!!) else null
curEpisode = media.episode
curEpisode = media.episodeOrFetch()
// curMedia = curEpisode?.media
} else curMedia = media

Expand Down
17 changes: 10 additions & 7 deletions app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,23 @@ import kotlinx.coroutines.*
object InTheatre {
val TAG: String = InTheatre::class.simpleName ?: "Anonymous"

var curIndexInQueue = -1

var curQueue: PlayQueue // unmanaged

var curEpisode: Episode? = null // unmanged
set(value) {
field = value
if (curMedia != field?.media) curMedia = field?.media
field = if (value != null) unmanaged(value) else null
if (field?.media != null && curMedia?.getIdentifier() != field?.media?.getIdentifier()) curMedia = unmanaged(field!!.media!!)
}

var curMedia: Playable? = null // unmanged if EpisodeMedia
set(value) {
field = if (value != null && value is EpisodeMedia) unmanaged(value) else value
if (field is EpisodeMedia) {
val media = (field as EpisodeMedia)
if (curEpisode != media.episode) curEpisode = media.episode
if (value is EpisodeMedia) {
field = unmanaged(value)
if (value.episode != null && curEpisode?.id != value.episode?.id) curEpisode = unmanaged(value.episode!!)
} else {
field = value
}
}

Expand Down Expand Up @@ -115,7 +118,7 @@ object InTheatre {
val mediaId = curState.curMediaId
if (mediaId != 0L) {
curMedia = getEpisodeMedia(mediaId)
if (curEpisode != null) curEpisode = (curMedia as EpisodeMedia).episode
if (curEpisode != null) curEpisode = (curMedia as EpisodeMedia).episodeOrFetch()
}
} else Log.e(TAG, "Could not restore Playable object from preferences")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
if (media != null) {
playbackSpeed = curState.curTempSpeed
if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL && media is EpisodeMedia) {
if (media.episode?.feed?.preferences != null) playbackSpeed = media.episode!!.feed!!.preferences!!.playSpeed
val prefs_ = media.episodeOrFetch()?.feed?.preferences
if (prefs_ != null) playbackSpeed = prefs_.playSpeed
}
}
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = getPlaybackSpeed(mediaType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
import ac.mdiq.podcini.net.utils.NetworkUtils.wasDownloadBlocked
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curIndexInQueue
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
import ac.mdiq.podcini.playback.base.PlayerStatus
Expand All @@ -15,11 +17,13 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.config.ClientConfig
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import ac.mdiq.podcini.util.event.FlowEvent.PlayEvent.Action
import ac.mdiq.podcini.util.showStackTrace
import android.app.UiModeManager
import android.content.Context
import android.content.res.Configuration
Expand Down Expand Up @@ -225,11 +229,18 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
*/
override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean, forceReset: Boolean) {
Logd(TAG, "playMediaObject status=$status stream=$stream startWhenPrepared=$startWhenPrepared prepareImmediately=$prepareImmediately forceReset=$forceReset ${playable.getEpisodeTitle()} ")
// showStackTrace()
if (curMedia != null) {
Logd(TAG, "playMediaObject: curMedia exist status=$status")
if (!forceReset && curMedia!!.getIdentifier() == prevMedia?.getIdentifier() && status == PlayerStatus.PLAYING) {
Logd(TAG, "Method call to playMediaObject was ignored: media file already playing.")
return
}
if (curMedia is EpisodeMedia) {
val media_ = curMedia as EpisodeMedia
curIndexInQueue = EpisodeUtil.indexOfItemWithId(curQueue.episodes, media_.id)
} else curIndexInQueue = -1

Logd(TAG, "playMediaObject starts new media playable:${playable.getIdentifier()} curMedia:${curMedia!!.getIdentifier()} prevMedia:${prevMedia?.getIdentifier()}")
// set temporarily to pause in order to update list with current position
if (status == PlayerStatus.PLAYING) {
Expand All @@ -241,12 +252,12 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
if (status == PlayerStatus.PAUSED || status == PlayerStatus.PLAYING || status == PlayerStatus.PREPARED) exoPlayer?.stop()
// if (prevMedia != null && curMedia!!.getIdentifier() != prevMedia?.getIdentifier())
// callback.onPostPlayback(prevMedia, ended = false, skipped = false, true)
prevMedia = curMedia
setPlayerStatus(PlayerStatus.INDETERMINATE, null)
}

Logd(TAG, "playMediaObject preparing for playable:${playable.getIdentifier()} ${playable.getEpisodeTitle()}")
curMedia = playable
prevMedia = curMedia
this.isStreaming = stream
mediaType = curMedia!!.getMediaType()
videoSize = null
Expand All @@ -264,7 +275,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
if (streamurl != null) {
val media = curMedia
if (media is EpisodeMedia) {
val preferences = media.episode?.feed?.preferences
val preferences = media.episodeOrFetch()?.feed?.preferences
setDataSource(metadata, streamurl, preferences?.username, preferences?.password)
} else setDataSource(metadata, streamurl, null, null)
}
Expand All @@ -289,6 +300,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
e.printStackTrace()
setPlayerStatus(PlayerStatus.ERROR, null)
EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(e.localizedMessage ?: ""))
} finally {
}
}

Expand Down Expand Up @@ -431,13 +443,15 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
var volumeLeft = volumeLeft
var volumeRight = volumeRight
val playable = curMedia
if (playable is EpisodeMedia && playable.episode?.feed?.preferences != null) {
val preferences = playable.episode!!.feed!!.preferences!!
val volumeAdaptionSetting = preferences.volumeAdaptionSetting
if (volumeAdaptionSetting != null) {
val adaptionFactor = volumeAdaptionSetting.adaptionFactor
volumeLeft *= adaptionFactor
volumeRight *= adaptionFactor
if (playable is EpisodeMedia) {
val preferences = playable.episodeOrFetch()?.feed?.preferences
if (preferences != null) {
val volumeAdaptionSetting = preferences.volumeAdaptionSetting
if (volumeAdaptionSetting != null) {
val adaptionFactor = volumeAdaptionSetting.adaptionFactor
volumeLeft *= adaptionFactor
volumeRight *= adaptionFactor
}
}
}
if (volumeLeft > 1) {
Expand Down Expand Up @@ -532,6 +546,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP

override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean) {
releaseWifiLockIfNecessary()
if (curMedia == null) return

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

0 comments on commit 5a40c6a

Please sign in to comment.