6.13.6 commit
This commit is contained in:
parent
bdf56d58b3
commit
b2501d6f2b
|
@ -26,8 +26,8 @@ android {
|
|||
vectorDrawables.useSupportLibrary false
|
||||
vectorDrawables.generatedDensities = []
|
||||
|
||||
versionCode 3020292
|
||||
versionName "6.13.5"
|
||||
versionCode 3020293
|
||||
versionName "6.13.6"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
@ -239,7 +239,6 @@ dependencies {
|
|||
|
||||
implementation libs.searchpreference
|
||||
implementation libs.balloon
|
||||
// implementation libs.stream
|
||||
|
||||
implementation libs.fyydlin
|
||||
|
||||
|
|
|
@ -131,7 +131,6 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
|
|||
Logd(TAG, "Starting download")
|
||||
try {
|
||||
while (!cancelled && (connection.read(buffer).also { count = it }) != -1) {
|
||||
// Log.d(TAG,"buffer: $buffer")
|
||||
out.write(buffer, 0, count)
|
||||
downloadRequest.soFar += count
|
||||
val progressPercent = (100.0 * downloadRequest.soFar / downloadRequest.size).toInt()
|
||||
|
|
|
@ -12,6 +12,7 @@ import ac.mdiq.podcini.net.utils.NetworkUtils.isFeedRefreshAllowed
|
|||
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.networkAvailable
|
||||
import ac.mdiq.podcini.net.utils.UrlChecker.prepareUrl
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
||||
|
@ -27,6 +28,7 @@ import ac.mdiq.podcini.util.EventFlow
|
|||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.config.ClientConfigurator
|
||||
import ac.mdiq.vista.extractor.InfoItem
|
||||
import ac.mdiq.vista.extractor.Vista
|
||||
import ac.mdiq.vista.extractor.channel.ChannelInfo
|
||||
import ac.mdiq.vista.extractor.channel.tabs.ChannelTabInfo
|
||||
|
@ -50,6 +52,8 @@ import com.google.common.util.concurrent.Futures
|
|||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import io.realm.kotlin.ext.realmListOf
|
||||
import io.realm.kotlin.types.RealmList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.xml.sax.SAXException
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
@ -67,6 +71,7 @@ object FeedUpdateManager {
|
|||
private const val WORK_ID_FEED_UPDATE_MANUAL = "feedUpdateManual"
|
||||
const val EXTRA_FEED_ID: String = "feed_id"
|
||||
const val EXTRA_NEXT_PAGE: String = "next_page"
|
||||
const val EXTRA_FULL_UPDATE: String = "full_update"
|
||||
const val EXTRA_EVEN_ON_MOBILE: String = "even_on_mobile"
|
||||
|
||||
private val updateInterval: Long
|
||||
|
@ -93,7 +98,7 @@ object FeedUpdateManager {
|
|||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun runOnce(context: Context, feed: Feed? = null, nextPage: Boolean = false) {
|
||||
fun runOnce(context: Context, feed: Feed? = null, nextPage: Boolean = false, fullUpdate: Boolean = false) {
|
||||
val workRequest: OneTimeWorkRequest.Builder = OneTimeWorkRequest.Builder(FeedUpdateWorker::class.java)
|
||||
.setInitialDelay(0L, TimeUnit.MILLISECONDS)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
|
@ -102,6 +107,7 @@ object FeedUpdateManager {
|
|||
|
||||
val builder = Data.Builder()
|
||||
builder.putBoolean(EXTRA_EVEN_ON_MOBILE, true)
|
||||
if (fullUpdate) builder.putBoolean(EXTRA_FULL_UPDATE, true)
|
||||
if (feed != null) {
|
||||
builder.putLong(EXTRA_FEED_ID, feed.id)
|
||||
builder.putBoolean(EXTRA_NEXT_PAGE, nextPage)
|
||||
|
@ -112,12 +118,12 @@ object FeedUpdateManager {
|
|||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun runOnceOrAsk(context: Context, feed: Feed? = null) {
|
||||
fun runOnceOrAsk(context: Context, feed: Feed? = null, fullUpdate: Boolean = false) {
|
||||
Logd(TAG, "Run auto update immediately in background.")
|
||||
when {
|
||||
feed != null && feed.isLocalFeed -> runOnce(context, feed)
|
||||
feed != null && feed.isLocalFeed -> runOnce(context, feed, fullUpdate = fullUpdate)
|
||||
!networkAvailable() -> EventFlow.postEvent(FlowEvent.MessageEvent(context.getString(R.string.download_error_no_connection)))
|
||||
isFeedRefreshAllowed -> runOnce(context, feed)
|
||||
isFeedRefreshAllowed -> runOnce(context, feed, fullUpdate = fullUpdate)
|
||||
else -> confirmMobileRefresh(context, feed)
|
||||
}
|
||||
}
|
||||
|
@ -169,7 +175,8 @@ object FeedUpdateManager {
|
|||
return Result.retry()
|
||||
}
|
||||
}
|
||||
refreshFeeds(feedsToUpdate, force)
|
||||
val fullUpdate = inputData.getBoolean(EXTRA_FULL_UPDATE, false)
|
||||
refreshFeeds(feedsToUpdate, force, fullUpdate)
|
||||
notificationManager.cancel(R.id.notification_updating_feeds)
|
||||
autodownloadEpisodeMedia(applicationContext, feedsToUpdate.toList())
|
||||
feedsToUpdate.clear()
|
||||
|
@ -197,7 +204,7 @@ object FeedUpdateManager {
|
|||
return Futures.immediateFuture(ForegroundInfo(R.id.notification_updating_feeds, createNotification(null)))
|
||||
}
|
||||
|
||||
private fun refreshFeeds(feedsToUpdate: MutableList<Feed>, force: Boolean) {
|
||||
private fun refreshFeeds(feedsToUpdate: MutableList<Feed>, force: Boolean, fullUpdate: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this.applicationContext,
|
||||
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
// TODO: Consider calling
|
||||
|
@ -222,8 +229,8 @@ object FeedUpdateManager {
|
|||
Logd(TAG, "updating local feed? ${feed.isLocalFeed} ${feed.title}")
|
||||
when {
|
||||
feed.isLocalFeed -> LocalFeedUpdater.updateFeed(feed, applicationContext, null)
|
||||
feed.type == Feed.FeedType.YOUTUBE.name -> refreshYoutubeFeed(feed)
|
||||
else -> refreshFeed(feed, force)
|
||||
feed.type == Feed.FeedType.YOUTUBE.name -> refreshYoutubeFeed(feed, fullUpdate)
|
||||
else -> refreshFeed(feed, force, fullUpdate)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logd(TAG, "update failed ${e.message}")
|
||||
|
@ -234,23 +241,44 @@ object FeedUpdateManager {
|
|||
titles.removeAt(0)
|
||||
}
|
||||
}
|
||||
private fun refreshYoutubeFeed(feed: Feed) {
|
||||
private fun refreshYoutubeFeed(feed: Feed, fullUpdate: Boolean) {
|
||||
if (feed.downloadUrl.isNullOrEmpty()) return
|
||||
val url = feed.downloadUrl
|
||||
val url = feed.downloadUrl!!
|
||||
val newestItem = feed.mostRecentItem
|
||||
val oldestItem = feed.oldestItem
|
||||
try {
|
||||
val service = try { Vista.getService("YouTube")
|
||||
} catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
|
||||
|
||||
val service = try { Vista.getService("YouTube") } catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
|
||||
val uURL = URL(url)
|
||||
if (uURL.path.startsWith("/channel")) {
|
||||
val channelInfo = ChannelInfo.getInfo(service, url!!)
|
||||
val channelInfo = ChannelInfo.getInfo(service, url)
|
||||
Logd(TAG, "refreshYoutubeFeed channelInfo: $channelInfo ${channelInfo.tabs.size}")
|
||||
if (channelInfo.tabs.isEmpty()) return
|
||||
try {
|
||||
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first())
|
||||
Logd(TAG, "refreshYoutubeFeed result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
|
||||
var infoItems = channelTabInfo.relatedItems
|
||||
var nextPage = channelTabInfo.nextPage
|
||||
val eList: RealmList<Episode> = realmListOf()
|
||||
for (r in channelTabInfo.relatedItems) eList.add(episodeFromStreamInfoItem(r as StreamInfoItem))
|
||||
while (infoItems.isNotEmpty()) {
|
||||
for (r_ in infoItems) {
|
||||
val r = r_ as StreamInfoItem
|
||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||
Logd(TAG, "item: ${r.uploadDate?.date()?.time} ${r.name}")
|
||||
if ((r.uploadDate?.date()?.time ?: Date(0)) > (newestItem?.getPubDate() ?: Date(0)))
|
||||
eList.add(episodeFromStreamInfoItem(r))
|
||||
else nextPage = null
|
||||
}
|
||||
if (nextPage == null) break
|
||||
try {
|
||||
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
|
||||
nextPage = page.nextPage
|
||||
infoItems = page.items
|
||||
Logd(TAG, "refreshYoutubeFeed more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "refreshYoutubeFeed ChannelTabInfo.getMoreItems error: ${e.message}")
|
||||
break
|
||||
}
|
||||
}
|
||||
val feed_ = Feed(url, null)
|
||||
feed_.type = Feed.FeedType.YOUTUBE.name
|
||||
feed_.hasVideoMedia = true
|
||||
|
@ -260,13 +288,32 @@ object FeedUpdateManager {
|
|||
feed_.author = channelInfo.parentChannelName
|
||||
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||
feed_.episodes = eList
|
||||
Feeds.updateFeed(applicationContext, feed_, false)
|
||||
if (fullUpdate) Feeds.updateFeed(applicationContext, feed_) else Feeds.updateFeedSimple(feed_)
|
||||
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed channel error1 ${e.message}") }
|
||||
} else if (uURL.path.startsWith("/playlist")) {
|
||||
val playlistInfo = PlaylistInfo.getInfo(Vista.getService(0), url) ?: return
|
||||
val playlistInfo = PlaylistInfo.getInfo(service, url) ?: return
|
||||
val eList: RealmList<Episode> = realmListOf()
|
||||
try {
|
||||
for (r in playlistInfo.relatedItems) eList.add(episodeFromStreamInfoItem(r))
|
||||
var infoItems = playlistInfo.relatedItems
|
||||
var nextPage = playlistInfo.nextPage
|
||||
while (infoItems.isNotEmpty()) {
|
||||
for (r in infoItems) {
|
||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||
if ((r.uploadDate?.date()?.time ?: Date(0)) > (newestItem?.getPubDate() ?: Date(0)))
|
||||
eList.add(episodeFromStreamInfoItem(r))
|
||||
else nextPage = null
|
||||
}
|
||||
if (nextPage == null) break
|
||||
try {
|
||||
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
|
||||
nextPage = page.nextPage
|
||||
infoItems = page.items
|
||||
Logd(TAG, "more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "PlaylistInfo.getMoreItems error: ${e.message}")
|
||||
break
|
||||
}
|
||||
}
|
||||
val feed_ = Feed(url, null)
|
||||
feed_.type = Feed.FeedType.YOUTUBE.name
|
||||
feed_.hasVideoMedia = true
|
||||
|
@ -276,14 +323,68 @@ object FeedUpdateManager {
|
|||
feed_.author = playlistInfo.uploaderName
|
||||
feed_.imageUrl = if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
|
||||
feed_.episodes = eList
|
||||
Feeds.updateFeed(applicationContext, feed_, false)
|
||||
if (fullUpdate) Feeds.updateFeed(applicationContext, feed_) else Feeds.updateFeedSimple(feed_)
|
||||
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed playlist error1 ${e.message}") }
|
||||
} else {
|
||||
val pathSegments = uURL.path.split("/")
|
||||
val channelUrl = "https://www.youtube.com/channel/${pathSegments[1]}"
|
||||
Logd(TAG, "channelUrl: $channelUrl")
|
||||
val channelInfo = ChannelInfo.getInfo(service, channelUrl)
|
||||
Logd(TAG, "refreshYoutubeFeed channelInfo: $channelInfo ${channelInfo.tabs.size}")
|
||||
if (channelInfo.tabs.isEmpty()) return
|
||||
var index = -1
|
||||
for (i in channelInfo.tabs.indices) {
|
||||
val url_ = prepareUrl(channelInfo.tabs[i].url)
|
||||
if (feed.downloadUrl == url_) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (index < 0) return
|
||||
try {
|
||||
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs[index])
|
||||
Logd(TAG, "refreshYoutubeFeed result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
|
||||
var infoItems = channelTabInfo.relatedItems
|
||||
var nextPage = channelTabInfo.nextPage
|
||||
val eList: RealmList<Episode> = realmListOf()
|
||||
while (infoItems.isNotEmpty()) {
|
||||
for (r_ in infoItems) {
|
||||
val r = r_ as StreamInfoItem
|
||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||
Logd(TAG, "item: ${r.uploadDate?.date()?.time} ${r.name}")
|
||||
if ((r.uploadDate?.date()?.time ?: Date(0)) > (newestItem?.getPubDate() ?: Date(0)))
|
||||
eList.add(episodeFromStreamInfoItem(r))
|
||||
else nextPage = null
|
||||
}
|
||||
if (nextPage == null) break
|
||||
try {
|
||||
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs[index], nextPage)
|
||||
nextPage = page.nextPage
|
||||
infoItems = page.items
|
||||
Logd(TAG, "refreshYoutubeFeed more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "refreshYoutubeFeed ChannelTabInfo.getMoreItems error: ${e.message}")
|
||||
break
|
||||
}
|
||||
}
|
||||
Logd(TAG, "refreshYoutubeFeed eList.size: ${eList.size}")
|
||||
val feed_ = Feed(url, null)
|
||||
feed_.type = Feed.FeedType.YOUTUBE.name
|
||||
feed_.hasVideoMedia = true
|
||||
feed_.title = channelInfo.name
|
||||
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
|
||||
feed_.description = channelInfo.description
|
||||
feed_.author = channelInfo.parentChannelName
|
||||
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||
feed_.episodes = eList
|
||||
if (fullUpdate) Feeds.updateFeed(applicationContext, feed_) else Feeds.updateFeedSimple(feed_)
|
||||
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed channel error2 ${e.message}") }
|
||||
}
|
||||
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed error ${e.message}") }
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun refreshFeed(feed: Feed, force: Boolean) {
|
||||
fun refreshFeed(feed: Feed, force: Boolean, fullUpdate: Boolean) {
|
||||
val nextPage = (inputData.getBoolean(EXTRA_NEXT_PAGE, false) && feed.nextPageLink != null)
|
||||
if (nextPage) feed.pageNr += 1
|
||||
val builder = create(feed)
|
||||
|
@ -300,7 +401,7 @@ object FeedUpdateManager {
|
|||
return
|
||||
}
|
||||
val feedUpdateTask = FeedUpdateTask(applicationContext, request)
|
||||
val success = feedUpdateTask.run()
|
||||
val success = if (fullUpdate) feedUpdateTask.run() else feedUpdateTask.runSimple()
|
||||
if (!success) {
|
||||
Logd(TAG, "update failed: unsuccessful")
|
||||
Feeds.persistFeedLastUpdateFailed(feed, true)
|
||||
|
@ -423,6 +524,14 @@ object FeedUpdateManager {
|
|||
Feeds.updateFeed(context, feedHandlerResult!!.feed, false)
|
||||
return true
|
||||
}
|
||||
|
||||
fun runSimple(): Boolean {
|
||||
feedHandlerResult = task.call()
|
||||
if (!task.isSuccessful) return false
|
||||
Feeds.updateFeedSimple(feedHandlerResult!!.feed)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
package ac.mdiq.podcini.playback.base
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
||||
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.preferences.UserPreferences.isSkipSilence
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.prefLowQualityMedia
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
|
@ -21,15 +18,10 @@ import ac.mdiq.podcini.util.FlowEvent
|
|||
import ac.mdiq.podcini.util.FlowEvent.PlayEvent.Action
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.config.ClientConfig
|
||||
import ac.mdiq.vista.extractor.MediaFormat
|
||||
import ac.mdiq.vista.extractor.stream.AudioStream
|
||||
import ac.mdiq.vista.extractor.stream.DeliveryMethod
|
||||
import ac.mdiq.vista.extractor.stream.VideoStream
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.media.audiofx.LoudnessEnhancer
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.util.Pair
|
||||
import android.view.SurfaceHolder
|
||||
|
@ -37,23 +29,15 @@ import androidx.core.util.Consumer
|
|||
import androidx.media3.common.*
|
||||
import androidx.media3.common.Player.*
|
||||
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.HttpDataSource.HttpDataSourceException
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.SeekParameters
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride
|
||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection
|
||||
import androidx.media3.extractor.DefaultExtractorsFactory
|
||||
import androidx.media3.extractor.mp3.Mp3Extractor
|
||||
import androidx.media3.ui.DefaultTrackNameProvider
|
||||
import androidx.media3.ui.TrackNameProvider
|
||||
import kotlinx.coroutines.*
|
||||
|
@ -75,8 +59,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
private var seekLatch: CountDownLatch? = null
|
||||
|
||||
private val bufferUpdateInterval = 5000L
|
||||
private var mediaSource: MediaSource? = null
|
||||
private var mediaItem: MediaItem? = null
|
||||
private var playbackParameters: PlaybackParameters
|
||||
|
||||
private var bufferedPercentagePrev = 0
|
||||
|
@ -113,8 +95,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
init {
|
||||
if (httpDataSourceFactory == null) {
|
||||
runOnIOScope {
|
||||
httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory)
|
||||
.setUserAgent(ClientConfig.USER_AGENT)
|
||||
httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory).setUserAgent(ClientConfig.USER_AGENT)
|
||||
}
|
||||
}
|
||||
if (exoPlayer == null) {
|
||||
|
@ -164,130 +145,15 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
exoPlayer?.setAudioAttributes(b.build(), true)
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class)
|
||||
private fun setDataSource(metadata: MediaMetadata, mediaUrl: String, user: String?, password: String?) {
|
||||
Logd(TAG, "setDataSource: $mediaUrl")
|
||||
mediaItem = MediaItem.Builder().setUri(Uri.parse(mediaUrl)).setMediaMetadata(metadata).build()
|
||||
mediaSource = null
|
||||
setSourceCredentials(user, password)
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class)
|
||||
private fun setDataSource(metadata: MediaMetadata, media: EpisodeMedia) {
|
||||
Logd(TAG, "setDataSource1 called")
|
||||
val url = media.getStreamUrl() ?: return
|
||||
val preferences = media.episodeOrFetch()?.feed?.preferences
|
||||
val user = preferences?.username
|
||||
val password = preferences?.password
|
||||
if (media.episode?.feed?.type == Feed.FeedType.YOUTUBE.name) {
|
||||
Logd(TAG, "setDataSource1 setting for YouTube source")
|
||||
try {
|
||||
// val vService = Vista.getService(0)
|
||||
val streamInfo = media.episode!!.streamInfo ?: return
|
||||
val audioStreamsList = getFilteredAudioStreams(streamInfo.audioStreams)
|
||||
Logd(TAG, "setDataSource1 audioStreamsList ${audioStreamsList.size}")
|
||||
val audioIndex = if (isNetworkRestricted && prefLowQualityMedia) 0 else audioStreamsList.size - 1
|
||||
val audioStream = audioStreamsList[audioIndex]
|
||||
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate} forceVideo: ${media.forceVideo}")
|
||||
val aSource = DefaultMediaSourceFactory(context).createMediaSource(
|
||||
MediaItem.Builder().setMediaMetadata(metadata).setTag(metadata).setUri(Uri.parse(audioStream.content)).build())
|
||||
if (media.forceVideo || media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
|
||||
Logd(TAG, "setDataSource1 result: $streamInfo")
|
||||
Logd(TAG, "setDataSource1 videoStreams: ${streamInfo.videoStreams.size} videoOnlyStreams: ${streamInfo.videoOnlyStreams.size} audioStreams: ${streamInfo.audioStreams.size}")
|
||||
val videoStreamsList = getSortedStreamVideosList(streamInfo.videoStreams, streamInfo.videoOnlyStreams, true, true)
|
||||
val videoIndex = 0
|
||||
val videoStream = videoStreamsList[videoIndex]
|
||||
Logd(TAG, "setDataSource1 use video quality: ${videoStream.resolution}")
|
||||
val vSource = DefaultMediaSourceFactory(context).createMediaSource(
|
||||
MediaItem.Builder().setMediaMetadata(metadata).setTag(metadata).setUri(Uri.parse(videoStream.content)).build())
|
||||
val mediaSources: MutableList<MediaSource> = ArrayList()
|
||||
mediaSources.add(vSource)
|
||||
mediaSources.add(aSource)
|
||||
mediaSource = MergingMediaSource(true, *mediaSources.toTypedArray<MediaSource>())
|
||||
// mediaSource = null
|
||||
} else mediaSource = aSource
|
||||
mediaItem = mediaSource?.mediaItem
|
||||
setSourceCredentials(user, password)
|
||||
} catch (throwable: Throwable) { Log.e(TAG, "setDataSource1 error: ${throwable.message}") }
|
||||
} else {
|
||||
Logd(TAG, "setDataSource1 setting for Podcast source")
|
||||
setDataSource(metadata, url,user, password)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSourceCredentials(user: String?, password: String?) {
|
||||
if (!user.isNullOrEmpty() && !password.isNullOrEmpty()) {
|
||||
if (httpDataSourceFactory == null)
|
||||
httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory)
|
||||
.setUserAgent(ClientConfig.USER_AGENT)
|
||||
|
||||
val requestProperties = HashMap<String, String>()
|
||||
requestProperties["Authorization"] = HttpCredentialEncoder.encode(user, password, "ISO-8859-1")
|
||||
httpDataSourceFactory!!.setDefaultRequestProperties(requestProperties)
|
||||
|
||||
val dataSourceFactory: DataSource.Factory = DefaultDataSource.Factory(context, httpDataSourceFactory!!)
|
||||
val extractorsFactory = DefaultExtractorsFactory()
|
||||
extractorsFactory.setConstantBitrateSeekingEnabled(true)
|
||||
extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_DISABLE_ID3_METADATA)
|
||||
val f = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||
|
||||
mediaSource = f.createMediaSource(mediaItem!!)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the two lists of video streams (video_only and normal videos),
|
||||
* and sort them according with default format chosen by the user.
|
||||
*
|
||||
* @param defaultFormat format to give preference
|
||||
* @param showHigherResolutions show >1080p resolutions
|
||||
* @param videoStreams normal videos list
|
||||
* @param videoOnlyStreams video only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @param preferVideoOnlyStreams if video-only streams should preferred when both video-only
|
||||
* streams and normal video streams are available
|
||||
* @return the sorted list
|
||||
*/
|
||||
private fun getSortedStreamVideosList(videoStreams: List<VideoStream>?, videoOnlyStreams: List<VideoStream>?, ascendingOrder: Boolean,
|
||||
preferVideoOnlyStreams: Boolean): List<VideoStream> {
|
||||
val videoStreamsOrdered = if (preferVideoOnlyStreams) listOf(videoStreams, videoOnlyStreams) else listOf(videoOnlyStreams, videoStreams)
|
||||
val allInitialStreams = videoStreamsOrdered.filterNotNull().flatten().toList()
|
||||
val comparator = compareBy<VideoStream> { it.resolution.toResolutionValue() }
|
||||
return if (ascendingOrder) allInitialStreams.sortedWith(comparator) else { allInitialStreams.sortedWith(comparator.reversed()) }
|
||||
}
|
||||
|
||||
private fun String.toResolutionValue(): Int {
|
||||
val match = Regex("(\\d+)p|(\\d+)k").find(this)
|
||||
return when {
|
||||
match?.groupValues?.get(1) != null -> match.groupValues[1].toInt()
|
||||
match?.groupValues?.get(2) != null -> match.groupValues[2].toInt() * 1024
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFilteredAudioStreams(audioStreams: List<AudioStream>?): List<AudioStream> {
|
||||
if (audioStreams == null) return listOf()
|
||||
val collectedStreams = mutableSetOf<AudioStream>()
|
||||
for (stream in audioStreams) {
|
||||
Logd(TAG, "getFilteredAudioStreams stream: ${stream.audioTrackId} ${stream.bitrate} ${stream.deliveryMethod} ${stream.format}")
|
||||
if (stream.deliveryMethod == DeliveryMethod.TORRENT || (stream.deliveryMethod == DeliveryMethod.HLS && stream.format == MediaFormat.OPUS))
|
||||
continue
|
||||
collectedStreams.add(stream)
|
||||
}
|
||||
return collectedStreams.toList().sortedWith(compareBy { it.bitrate })
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
|
||||
* episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
|
||||
* not do anything.
|
||||
* episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will not do anything.
|
||||
* Whether playback starts immediately depends on the given parameters. See below for more details.
|
||||
* States:
|
||||
* During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
|
||||
* If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If
|
||||
* 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
|
||||
* If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object
|
||||
* will enter the ERROR state.
|
||||
* If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state.
|
||||
* If 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
|
||||
* If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object will enter the ERROR state.
|
||||
* This method is executed on an internal executor service.
|
||||
* @param playable The Playable object that is supposed to be played. This parameter must not be null.
|
||||
* @param streaming The type of playback. If false, the Playable object MUST provide access to a locally available file via
|
||||
|
@ -397,7 +263,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
seekTo(newPosition)
|
||||
}
|
||||
if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepareWR()
|
||||
// while (mediaItem == null && mediaSource == null) runBlocking { delay(100) }
|
||||
exoPlayer?.play()
|
||||
// Can't set params when paused - so always set it on start in case they changed
|
||||
exoPlayer?.playbackParameters = playbackParameters
|
||||
|
@ -758,8 +623,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
const val BUFFERING_STARTED: Int = -1
|
||||
const val BUFFERING_ENDED: Int = -2
|
||||
|
||||
private var httpDataSourceFactory: OkHttpDataSource.Factory? = null
|
||||
|
||||
private var trackSelector: DefaultTrackSelector? = null
|
||||
|
||||
var exoPlayer: ExoPlayer? = null
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
package ac.mdiq.podcini.playback.base
|
||||
|
||||
//import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
|
||||
import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.Prefs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.prefLowQualityMedia
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.FeedPreferences
|
||||
import ac.mdiq.podcini.storage.model.MediaType
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.util.showStackTrace
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.config.ClientConfig
|
||||
import ac.mdiq.vista.extractor.MediaFormat
|
||||
import ac.mdiq.vista.extractor.stream.AudioStream
|
||||
import ac.mdiq.vista.extractor.stream.DeliveryMethod
|
||||
import ac.mdiq.vista.extractor.stream.VideoStream
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
|
@ -24,6 +30,15 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.extractor.DefaultExtractorsFactory
|
||||
import androidx.media3.extractor.mp3.Mp3Extractor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.concurrent.Volatile
|
||||
|
@ -44,6 +59,9 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
private var oldStatus: PlayerStatus? = null
|
||||
internal var prevMedia: Playable? = null
|
||||
|
||||
protected var mediaSource: MediaSource? = null
|
||||
protected var mediaItem: MediaItem? = null
|
||||
|
||||
internal var mediaType: MediaType = MediaType.UNKNOWN
|
||||
internal val startWhenPrepared = AtomicBoolean(false)
|
||||
|
||||
|
@ -96,6 +114,128 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
|
||||
open fun createMediaPlayer() {}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class)
|
||||
protected fun setDataSource(metadata: MediaMetadata, mediaUrl: String, user: String?, password: String?) {
|
||||
Logd(TAG, "setDataSource: $mediaUrl")
|
||||
mediaItem = MediaItem.Builder().setUri(Uri.parse(mediaUrl)).setMediaMetadata(metadata).build()
|
||||
mediaSource = null
|
||||
setSourceCredentials(user, password)
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class)
|
||||
protected fun setDataSource(metadata: MediaMetadata, media: EpisodeMedia) {
|
||||
Logd(TAG, "setDataSource1 called")
|
||||
val url = media.getStreamUrl() ?: return
|
||||
val preferences = media.episodeOrFetch()?.feed?.preferences
|
||||
val user = preferences?.username
|
||||
val password = preferences?.password
|
||||
if (media.episode?.feed?.type == Feed.FeedType.YOUTUBE.name) {
|
||||
Logd(TAG, "setDataSource1 setting for YouTube source")
|
||||
try {
|
||||
val streamInfo = media.episode!!.streamInfo ?: return
|
||||
val audioStreamsList = getFilteredAudioStreams(streamInfo.audioStreams)
|
||||
Logd(TAG, "setDataSource1 audioStreamsList ${audioStreamsList.size}")
|
||||
val audioIndex = if (isNetworkRestricted && prefLowQualityMedia && media.episode?.feed?.preferences?.audioQualitySetting == FeedPreferences.AVQuality.GLOBAL) 0 else {
|
||||
when (media.episode?.feed?.preferences?.audioQualitySetting) {
|
||||
FeedPreferences.AVQuality.LOW -> 0
|
||||
FeedPreferences.AVQuality.MEDIUM -> audioStreamsList.size / 2
|
||||
FeedPreferences.AVQuality.HIGH -> audioStreamsList.size - 1
|
||||
else -> audioStreamsList.size - 1
|
||||
}
|
||||
}
|
||||
val audioStream = audioStreamsList[audioIndex]
|
||||
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate} forceVideo: ${media.forceVideo}")
|
||||
val aSource = DefaultMediaSourceFactory(context).createMediaSource(
|
||||
MediaItem.Builder().setMediaMetadata(metadata).setTag(metadata).setUri(Uri.parse(audioStream.content)).build())
|
||||
if (media.forceVideo || media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
|
||||
Logd(TAG, "setDataSource1 result: $streamInfo")
|
||||
Logd(TAG, "setDataSource1 videoStreams: ${streamInfo.videoStreams.size} videoOnlyStreams: ${streamInfo.videoOnlyStreams.size} audioStreams: ${streamInfo.audioStreams.size}")
|
||||
val videoStreamsList = getSortedStreamVideosList(streamInfo.videoStreams, streamInfo.videoOnlyStreams, true, true)
|
||||
val videoIndex = if (isNetworkRestricted && prefLowQualityMedia && media.episode?.feed?.preferences?.videoQualitySetting == FeedPreferences.AVQuality.GLOBAL) 0 else {
|
||||
when (media.episode?.feed?.preferences?.videoQualitySetting) {
|
||||
FeedPreferences.AVQuality.LOW -> 0
|
||||
FeedPreferences.AVQuality.MEDIUM -> videoStreamsList.size / 2
|
||||
FeedPreferences.AVQuality.HIGH -> videoStreamsList.size - 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
val videoStream = videoStreamsList[videoIndex]
|
||||
Logd(TAG, "setDataSource1 use video quality: ${videoStream.resolution}")
|
||||
val vSource = DefaultMediaSourceFactory(context).createMediaSource(
|
||||
MediaItem.Builder().setMediaMetadata(metadata).setTag(metadata).setUri(Uri.parse(videoStream.content)).build())
|
||||
val mediaSources: MutableList<MediaSource> = ArrayList()
|
||||
mediaSources.add(vSource)
|
||||
mediaSources.add(aSource)
|
||||
mediaSource = MergingMediaSource(true, *mediaSources.toTypedArray<MediaSource>())
|
||||
// mediaSource = null
|
||||
} else mediaSource = aSource
|
||||
mediaItem = mediaSource?.mediaItem
|
||||
setSourceCredentials(user, password)
|
||||
} catch (throwable: Throwable) { Log.e(TAG, "setDataSource1 error: ${throwable.message}") }
|
||||
} else {
|
||||
Logd(TAG, "setDataSource1 setting for Podcast source")
|
||||
setDataSource(metadata, url,user, password)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSourceCredentials(user: String?, password: String?) {
|
||||
if (!user.isNullOrEmpty() && !password.isNullOrEmpty()) {
|
||||
if (httpDataSourceFactory == null)
|
||||
httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory)
|
||||
.setUserAgent(ClientConfig.USER_AGENT)
|
||||
|
||||
val requestProperties = HashMap<String, String>()
|
||||
requestProperties["Authorization"] = HttpCredentialEncoder.encode(user, password, "ISO-8859-1")
|
||||
httpDataSourceFactory!!.setDefaultRequestProperties(requestProperties)
|
||||
|
||||
val dataSourceFactory: DataSource.Factory = DefaultDataSource.Factory(context, httpDataSourceFactory!!)
|
||||
val extractorsFactory = DefaultExtractorsFactory()
|
||||
extractorsFactory.setConstantBitrateSeekingEnabled(true)
|
||||
extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_DISABLE_ID3_METADATA)
|
||||
val f = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||
|
||||
mediaSource = f.createMediaSource(mediaItem!!)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the two lists of video streams (video_only and normal videos),
|
||||
* and sort them according with default format chosen by the user.
|
||||
* @param videoStreams normal videos list
|
||||
* @param videoOnlyStreams video only stream list
|
||||
* @param ascendingOrder true -> smallest to greatest | false -> greatest to smallest
|
||||
* @param preferVideoOnlyStreams if video-only streams should preferred when both video-only streams and normal video streams are available
|
||||
* @return the sorted list
|
||||
*/
|
||||
private fun getSortedStreamVideosList(videoStreams: List<VideoStream>?, videoOnlyStreams: List<VideoStream>?, ascendingOrder: Boolean,
|
||||
preferVideoOnlyStreams: Boolean): List<VideoStream> {
|
||||
val videoStreamsOrdered = if (preferVideoOnlyStreams) listOf(videoStreams, videoOnlyStreams) else listOf(videoOnlyStreams, videoStreams)
|
||||
val allInitialStreams = videoStreamsOrdered.filterNotNull().flatten().toList()
|
||||
val comparator = compareBy<VideoStream> { it.resolution.toResolutionValue() }
|
||||
return if (ascendingOrder) allInitialStreams.sortedWith(comparator) else { allInitialStreams.sortedWith(comparator.reversed()) }
|
||||
}
|
||||
|
||||
private fun String.toResolutionValue(): Int {
|
||||
val match = Regex("(\\d+)p|(\\d+)k").find(this)
|
||||
return when {
|
||||
match?.groupValues?.get(1) != null -> match.groupValues[1].toInt()
|
||||
match?.groupValues?.get(2) != null -> match.groupValues[2].toInt() * 1024
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFilteredAudioStreams(audioStreams: List<AudioStream>?): List<AudioStream> {
|
||||
if (audioStreams == null) return listOf()
|
||||
val collectedStreams = mutableSetOf<AudioStream>()
|
||||
for (stream in audioStreams) {
|
||||
Logd(TAG, "getFilteredAudioStreams stream: ${stream.audioTrackId} ${stream.bitrate} ${stream.deliveryMethod} ${stream.format}")
|
||||
if (stream.deliveryMethod == DeliveryMethod.TORRENT || (stream.deliveryMethod == DeliveryMethod.HLS && stream.format == MediaFormat.OPUS))
|
||||
continue
|
||||
collectedStreams.add(stream)
|
||||
}
|
||||
return collectedStreams.toList().sortedWith(compareBy { it.bitrate })
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
|
||||
* episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
|
||||
|
@ -292,7 +432,6 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
private val TAG: String = MediaPlayerBase::class.simpleName ?: "Anonymous"
|
||||
|
||||
@get:Synchronized
|
||||
// @Volatile
|
||||
@JvmStatic
|
||||
var status by mutableStateOf(PlayerStatus.STOPPED)
|
||||
|
||||
|
@ -310,6 +449,9 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
@JvmField
|
||||
val LONG_REWIND: Long = TimeUnit.SECONDS.toMillis(20)
|
||||
|
||||
@JvmStatic
|
||||
var httpDataSourceFactory: OkHttpDataSource.Factory? = null
|
||||
|
||||
val prefPlaybackSpeed: Float
|
||||
get() {
|
||||
try { return appPrefs.getString(Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
|
||||
|
|
|
@ -42,6 +42,9 @@ import kotlin.math.min
|
|||
object Episodes {
|
||||
private val TAG: String = Episodes::class.simpleName ?: "Anonymous"
|
||||
|
||||
val prefRemoveFromQueueMarkedPlayed by lazy { appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true) }
|
||||
val prefDeleteRemovesFromQueue by lazy { appPrefs.getBoolean(Prefs.prefDeleteRemovesFromQueue.name, false) }
|
||||
|
||||
/**
|
||||
* @param offset The first episode that should be loaded.
|
||||
* @param limit The maximum number of episodes that should be loaded.
|
||||
|
@ -64,13 +67,6 @@ object Episodes {
|
|||
return realm.query(Episode::class).query(queryString).count().find().toInt()
|
||||
}
|
||||
|
||||
// used in tests only
|
||||
fun getEpisode(itemId: Long): Episode? {
|
||||
Logd(TAG, "getFeedItem called with id $itemId")
|
||||
val it = realm.query(Episode::class).query("id == $0", itemId).first().find()
|
||||
return if (it != null) realm.copyFromRealm(it) else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific FeedItem from the database.
|
||||
* @param guid feed episode guid
|
||||
|
@ -104,12 +100,6 @@ object Episodes {
|
|||
}
|
||||
}
|
||||
|
||||
val prefDeleteRemovesFromQueue: Boolean
|
||||
get() = appPrefs.getBoolean(Prefs.prefDeleteRemovesFromQueue.name, false)
|
||||
// fun shouldDeleteRemoveFromQueue(): Boolean {
|
||||
// return appPrefs.getBoolean(Prefs.prefDeleteRemovesFromQueue.name, false)
|
||||
// }
|
||||
|
||||
fun deleteMediaSync(context: Context, episode: Episode): Episode {
|
||||
Logd(TAG, "deleteMediaSync called")
|
||||
val media = episode.media ?: return episode
|
||||
|
@ -179,7 +169,6 @@ object Episodes {
|
|||
* Remove the listed episodes and their EpisodeMedia entries.
|
||||
* Deleting media also removes the download log entries.
|
||||
*/
|
||||
|
||||
fun deleteEpisodes(context: Context, episodes: List<Episode>) : Job {
|
||||
return runOnIOScope {
|
||||
val removedFromQueue: MutableList<Episode> = mutableListOf()
|
||||
|
@ -224,7 +213,6 @@ object Episodes {
|
|||
* @param episodes the FeedItems.
|
||||
* @param resetMediaPosition true if this method should also reset the position of the Episode's EpisodeMedia object.
|
||||
*/
|
||||
|
||||
fun setPlayState(played: Int, resetMediaPosition: Boolean, vararg episodes: Episode) : Job {
|
||||
Logd(TAG, "setPlayState called")
|
||||
return runOnIOScope {
|
||||
|
@ -253,13 +241,6 @@ object Episodes {
|
|||
return result
|
||||
}
|
||||
|
||||
val prefRemoveFromQueueMarkedPlayed: Boolean
|
||||
get() = appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true)
|
||||
|
||||
// fun shouldMarkedPlayedRemoveFromQueues(): Boolean {
|
||||
// return appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true)
|
||||
// }
|
||||
|
||||
fun episodeFromStreamInfoItem(item: StreamInfoItem): Episode {
|
||||
val e = Episode()
|
||||
e.link = item.url
|
||||
|
|
|
@ -201,7 +201,7 @@ object Feeds {
|
|||
* @return The updated Feed from the database if it already existed, or the new Feed from the parameters otherwise.
|
||||
*/
|
||||
@Synchronized
|
||||
fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? {
|
||||
fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean= false): Feed? {
|
||||
Logd(TAG, "updateFeed called")
|
||||
var resultFeed: Feed?
|
||||
// val unlistedItems: MutableList<Episode> = ArrayList()
|
||||
|
@ -237,8 +237,6 @@ object Feeds {
|
|||
val priorMostRecent = savedFeed.mostRecentItem
|
||||
val priorMostRecentDate: Date? = priorMostRecent?.getPubDate()
|
||||
var idLong = Feed.newId()
|
||||
Logd(TAG, "updateFeed building newFeedAssistant")
|
||||
val newFeedAssistant = FeedAssistant(newFeed, savedFeed.id)
|
||||
Logd(TAG, "updateFeed building savedFeedAssistant")
|
||||
val savedFeedAssistant = FeedAssistant(savedFeed)
|
||||
|
||||
|
@ -293,7 +291,6 @@ object Feeds {
|
|||
if (pubDate == null || priorMostRecentDate == null || priorMostRecentDate.before(pubDate) || priorMostRecentDate == pubDate) {
|
||||
Logd(TAG, "Marking episode published on $pubDate new, prior most recent date = $priorMostRecentDate")
|
||||
episode.playState = PlayState.NEW.code
|
||||
// episode.setNew()
|
||||
if (savedFeed.preferences?.autoAddNewToQueue == true) {
|
||||
val q = savedFeed.preferences?.queue
|
||||
if (q != null) runOnIOScope { addToQueueSync(episode, q) }
|
||||
|
@ -306,6 +303,8 @@ object Feeds {
|
|||
val unlistedItems: MutableList<Episode> = ArrayList()
|
||||
// identify episodes to be removed
|
||||
if (removeUnlistedItems) {
|
||||
Logd(TAG, "updateFeed building newFeedAssistant")
|
||||
val newFeedAssistant = FeedAssistant(newFeed, savedFeed.id)
|
||||
val it = savedFeed.episodes.toMutableList().iterator()
|
||||
while (it.hasNext()) {
|
||||
val feedItem = it.next()
|
||||
|
@ -314,19 +313,17 @@ object Feeds {
|
|||
it.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
newFeedAssistant.clear()
|
||||
}
|
||||
|
||||
// update attributes
|
||||
savedFeed.lastUpdate = newFeed.lastUpdate
|
||||
savedFeed.type = newFeed.type
|
||||
savedFeed.lastUpdateFailed = false
|
||||
resultFeed = savedFeed
|
||||
|
||||
savedFeed.totleDuration = 0
|
||||
for (e in savedFeed.episodes) {
|
||||
savedFeed.totleDuration += e.media?.duration ?: 0
|
||||
}
|
||||
for (e in savedFeed.episodes) savedFeed.totleDuration += e.media?.duration ?: 0
|
||||
|
||||
resultFeed = savedFeed
|
||||
try {
|
||||
upsertBlk(savedFeed) {}
|
||||
if (removeUnlistedItems && unlistedItems.isNotEmpty()) runBlocking { deleteEpisodes(context, unlistedItems).join() }
|
||||
|
@ -335,6 +332,69 @@ object Feeds {
|
|||
return resultFeed
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateFeedSimple(newFeed: Feed): Feed? {
|
||||
Logd(TAG, "updateFeedSimple called")
|
||||
val savedFeed = searchFeedByIdentifyingValueOrID(newFeed, true) ?: return newFeed
|
||||
|
||||
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: ${newFeed.nextPageLink}")
|
||||
savedFeed.nextPageLink = newFeed.nextPageLink
|
||||
}
|
||||
val priorMostRecent = savedFeed.mostRecentItem
|
||||
val priorMostRecentDate: Date = priorMostRecent?.getPubDate() ?: Date(0)
|
||||
var idLong = Feed.newId()
|
||||
Logd(TAG, "updateFeedSimple building savedFeedAssistant")
|
||||
|
||||
// Look for new or updated Items
|
||||
for (idx in newFeed.episodes.indices) {
|
||||
val episode = newFeed.episodes[idx]
|
||||
if ((episode.getPubDate()?: Date(0)) <= priorMostRecentDate) continue
|
||||
|
||||
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.before(pubDate) || priorMostRecentDate == pubDate) {
|
||||
Logd(TAG, "Marking episode published on $pubDate new, prior most recent date = $priorMostRecentDate")
|
||||
episode.playState = PlayState.NEW.code
|
||||
if (savedFeed.preferences?.autoAddNewToQueue == true) {
|
||||
val q = savedFeed.preferences?.queue
|
||||
if (q != null) runOnIOScope { addToQueueSync(episode, q) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update attributes
|
||||
savedFeed.lastUpdate = newFeed.lastUpdate
|
||||
savedFeed.type = newFeed.type
|
||||
savedFeed.lastUpdateFailed = false
|
||||
savedFeed.totleDuration = 0
|
||||
for (e in savedFeed.episodes) savedFeed.totleDuration += e.media?.duration ?: 0
|
||||
|
||||
val resultFeed = savedFeed
|
||||
try {
|
||||
upsertBlk(savedFeed) {}
|
||||
} catch (e: InterruptedException) { e.printStackTrace()
|
||||
} catch (e: ExecutionException) { e.printStackTrace() }
|
||||
return resultFeed
|
||||
}
|
||||
|
||||
fun persistFeedLastUpdateFailed(feed: Feed, lastUpdateFailed: Boolean) : Job {
|
||||
Logd(TAG, "persistFeedLastUpdateFailed called")
|
||||
return runOnIOScope {
|
||||
|
|
|
@ -40,7 +40,7 @@ object RealmDB {
|
|||
SubscriptionLog::class,
|
||||
Chapter::class))
|
||||
.name("Podcini.realm")
|
||||
.schemaVersion(28)
|
||||
.schemaVersion(29)
|
||||
.migration({ mContext ->
|
||||
val oldRealm = mContext.oldRealm // old realm using the previous schema
|
||||
val newRealm = mContext.newRealm // new realm using the new schema
|
||||
|
|
|
@ -142,7 +142,11 @@ class Feed : RealmObject {
|
|||
|
||||
@Ignore
|
||||
val mostRecentItem: Episode?
|
||||
get() = realm.query(Episode::class).query("feedId == $id SORT(pubDate DESC)").first().find()
|
||||
get() = realm.query(Episode::class).query("feedId == $id SORT (pubDate DESC)").first().find()
|
||||
|
||||
@Ignore
|
||||
val oldestItem: Episode?
|
||||
get() = realm.query(Episode::class).query("feedId == $id SORT (pubDate ASC)").first().find()
|
||||
|
||||
@Ignore
|
||||
var title: String?
|
||||
|
|
|
@ -48,7 +48,7 @@ class FeedPreferences : EmbeddedRealmObject {
|
|||
field = value
|
||||
autoDelete = field.code
|
||||
}
|
||||
var autoDelete: Int = 0
|
||||
var autoDelete: Int = AutoDeleteAction.GLOBAL.code
|
||||
|
||||
@Ignore
|
||||
var volumeAdaptionSetting: VolumeAdaptionSetting = VolumeAdaptionSetting.OFF
|
||||
|
@ -57,7 +57,25 @@ class FeedPreferences : EmbeddedRealmObject {
|
|||
field = value
|
||||
volumeAdaption = field.toInteger()
|
||||
}
|
||||
var volumeAdaption: Int = 0
|
||||
var volumeAdaption: Int = VolumeAdaptionSetting.OFF.value
|
||||
|
||||
@Ignore
|
||||
var audioQualitySetting: AVQuality = AVQuality.GLOBAL
|
||||
get() = AVQuality.fromCode(audioQuality)
|
||||
set(value) {
|
||||
field = value
|
||||
audioQuality = field.code
|
||||
}
|
||||
var audioQuality: Int = AVQuality.GLOBAL.code
|
||||
|
||||
@Ignore
|
||||
var videoQualitySetting: AVQuality = AVQuality.GLOBAL
|
||||
get() = AVQuality.fromCode(videoQuality)
|
||||
set(value) {
|
||||
field = value
|
||||
videoQuality = field.code
|
||||
}
|
||||
var videoQuality: Int = AVQuality.GLOBAL.code
|
||||
|
||||
var prefStreamOverDownload: Boolean = false
|
||||
|
||||
|
@ -132,7 +150,7 @@ class FeedPreferences : EmbeddedRealmObject {
|
|||
field = value
|
||||
autoDLPolicyCode = value.code
|
||||
}
|
||||
var autoDLPolicyCode: Int = 0
|
||||
var autoDLPolicyCode: Int = AutoDownloadPolicy.ONLY_NEW.code
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
@ -197,6 +215,22 @@ class FeedPreferences : EmbeddedRealmObject {
|
|||
}
|
||||
}
|
||||
|
||||
enum class AVQuality(val code: Int, val tag: String) {
|
||||
GLOBAL(0, "Global"),
|
||||
LOW(1, "Low"),
|
||||
MEDIUM(5, "Medium"),
|
||||
HIGH(10, "High");
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: Int): AVQuality {
|
||||
return enumValues<AVQuality>().firstOrNull { it.code == code } ?: GLOBAL
|
||||
}
|
||||
fun fromTag(tag: String): AVQuality {
|
||||
return enumValues<AVQuality>().firstOrNull { it.tag == tag } ?: GLOBAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SPEED_USE_GLOBAL: Float = -1f
|
||||
const val TAG_ROOT: String = "#root"
|
||||
|
|
|
@ -2,7 +2,7 @@ package ac.mdiq.podcini.storage.model
|
|||
|
||||
import ac.mdiq.podcini.R
|
||||
|
||||
enum class VolumeAdaptionSetting(private val value: Int, @JvmField val adaptionFactor: Float, val resId: Int) {
|
||||
enum class VolumeAdaptionSetting(val value: Int, @JvmField val adaptionFactor: Float, val resId: Int) {
|
||||
OFF(0, 1.0f, R.string.feed_volume_reduction_off),
|
||||
LIGHT_REDUCTION(1, 0.5f, R.string.feed_volume_reduction_light),
|
||||
HEAVY_REDUCTION(2, 0.2f, R.string.feed_volume_reduction_heavy),
|
||||
|
|
|
@ -997,14 +997,15 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDi
|
|||
@Composable
|
||||
fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: MutableSet<EpisodeFilter.EpisodesFilterGroup> = mutableSetOf(),
|
||||
onDismissRequest: () -> Unit, onFilterChanged: (Set<String>) -> Unit) {
|
||||
val filterValues: MutableSet<String> = mutableSetOf()
|
||||
val filterValues = remember { filter?.properties ?: mutableSetOf() }
|
||||
Dialog(properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { onDismissRequest() }) {
|
||||
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
|
||||
dialogWindowProvider?.window?.let { window ->
|
||||
window.setGravity(Gravity.BOTTOM)
|
||||
window.setDimAmount(0f)
|
||||
}
|
||||
Surface(modifier = Modifier.fillMaxWidth().height(350.dp), color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||
|
|
|
@ -179,8 +179,8 @@ import java.util.concurrent.Semaphore
|
|||
runOnIOScope {
|
||||
val feed_ = realm.query(Feed::class, "id == ${feed!!.id}").first().find()
|
||||
if (feed_ != null) {
|
||||
upsert(feed_) { it.preferences?.filterString = filterValues.joinToString() }
|
||||
loadFeed()
|
||||
feed = upsert(feed_) { it.preferences?.filterString = filterValues.joinToString() }
|
||||
// loadFeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,23 +38,19 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.util.*
|
||||
|
@ -110,9 +106,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Switch(checked = checked, modifier = Modifier.height(24.dp),
|
||||
onCheckedChange = {
|
||||
checked = it
|
||||
feed = upsertBlk(feed!!) { f ->
|
||||
f.preferences?.keepUpdated = checked
|
||||
}
|
||||
feed = upsertBlk(feed!!) { f -> f.preferences?.keepUpdated = checked }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -142,21 +136,55 @@ class FeedSettingsFragment : Fragment() {
|
|||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Text(text = stringResource(R.string.pref_stream_over_download_title), style = MaterialTheme.typography.titleLarge, color = textColor)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
var checked by remember {
|
||||
mutableStateOf(feed?.preferences?.prefStreamOverDownload ?: false)
|
||||
}
|
||||
var checked by remember { mutableStateOf(feed?.preferences?.prefStreamOverDownload ?: false) }
|
||||
Switch(checked = checked, modifier = Modifier.height(24.dp),
|
||||
onCheckedChange = {
|
||||
checked = it
|
||||
feed = upsertBlk(feed!!) { f ->
|
||||
f.preferences?.prefStreamOverDownload = checked
|
||||
}
|
||||
feed = upsertBlk(feed!!) { f -> f.preferences?.prefStreamOverDownload = checked }
|
||||
}
|
||||
)
|
||||
}
|
||||
Text(text = stringResource(R.string.pref_stream_over_download_sum), style = MaterialTheme.typography.bodyMedium, color = textColor)
|
||||
}
|
||||
}
|
||||
if (feed?.type == Feed.FeedType.YOUTUBE.name) {
|
||||
// audio quality
|
||||
Column {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var selectedOption by remember { mutableStateOf(feed?.preferences?.audioQualitySetting?.tag ?: FeedPreferences.AVQuality.GLOBAL.tag) }
|
||||
if (showDialog) SetAudioQuality(showDialog, selectedOption = selectedOption, onDismissRequest = { showDialog = false })
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Text(text = stringResource(R.string.pref_feed_audio_quality), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
selectedOption = feed!!.preferences?.audioQualitySetting?.tag ?: FeedPreferences.AVQuality.GLOBAL.tag
|
||||
showDialog = true
|
||||
})
|
||||
)
|
||||
}
|
||||
Text(text = stringResource(R.string.pref_feed_audio_quality_sum), style = MaterialTheme.typography.bodyMedium, color = textColor)
|
||||
}
|
||||
if (feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
|
||||
// video quality
|
||||
Column {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var selectedOption by remember { mutableStateOf(feed?.preferences?.videoQualitySetting?.tag ?: FeedPreferences.AVQuality.GLOBAL.tag) }
|
||||
if (showDialog) SetVideoQuality(showDialog, selectedOption = selectedOption, onDismissRequest = { showDialog = false })
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_videocam), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Text(text = stringResource(R.string.pref_feed_video_quality), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
selectedOption = feed!!.preferences?.videoQualitySetting?.tag ?: FeedPreferences.AVQuality.GLOBAL.tag
|
||||
showDialog = true
|
||||
})
|
||||
)
|
||||
}
|
||||
Text(text = stringResource(R.string.pref_feed_video_quality_sum), style = MaterialTheme.typography.bodyMedium, color = textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
// associated queue
|
||||
Column {
|
||||
curPrefQueue = feed?.preferences?.queueTextExt ?: "Default"
|
||||
|
@ -187,9 +215,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Switch(checked = checked, modifier = Modifier.height(24.dp),
|
||||
onCheckedChange = {
|
||||
checked = it
|
||||
feed = upsertBlk(feed!!) { f ->
|
||||
f.preferences?.autoAddNewToQueue = checked
|
||||
}
|
||||
feed = upsertBlk(feed!!) { f -> f.preferences?.autoAddNewToQueue = checked }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -230,10 +256,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Icon(ImageVector.vectorResource(id = R.drawable.ic_playback_speed), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Text(text = stringResource(R.string.playback_speed), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
PlaybackSpeedDialog().show()
|
||||
})
|
||||
)
|
||||
modifier = Modifier.clickable(onClick = { PlaybackSpeedDialog().show() }))
|
||||
}
|
||||
Text(text = stringResource(R.string.pref_feed_playback_speed_sum), style = MaterialTheme.typography.bodyMedium, color = textColor)
|
||||
}
|
||||
|
@ -286,13 +309,11 @@ class FeedSettingsFragment : Fragment() {
|
|||
onCheckedChange = {
|
||||
audoDownloadChecked = it
|
||||
feed = upsertBlk(feed!!) { f -> f.preferences?.autoDownload = audoDownloadChecked }
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
if (!isEnableAutodownload) {
|
||||
if (!isEnableAutodownload)
|
||||
Text(text = stringResource(R.string.auto_download_disabled_globally), style = MaterialTheme.typography.bodyMedium, color = textColor)
|
||||
}
|
||||
}
|
||||
if (audoDownloadChecked) {
|
||||
// auto download policy
|
||||
Column (modifier = Modifier.padding(start = 20.dp)){
|
||||
|
@ -300,10 +321,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
val showDialog = remember { mutableStateOf(false) }
|
||||
if (showDialog.value) AutoDownloadPolicyDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
|
||||
Text(text = stringResource(R.string.feed_auto_download_policy), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
showDialog.value = true
|
||||
})
|
||||
)
|
||||
modifier = Modifier.clickable(onClick = { showDialog.value = true }))
|
||||
}
|
||||
}
|
||||
// episode cache
|
||||
|
@ -312,8 +330,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
val showDialog = remember { mutableStateOf(false) }
|
||||
if (showDialog.value) SetEpisodesCacheDialog(showDialog.value, onDismiss = { showDialog.value = false })
|
||||
Text(text = stringResource(R.string.pref_episode_cache_title), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
modifier = Modifier.clickable(onClick = { showDialog.value = true })
|
||||
)
|
||||
modifier = Modifier.clickable(onClick = { showDialog.value = true }))
|
||||
}
|
||||
Text(text = stringResource(R.string.pref_episode_cache_summary), style = MaterialTheme.typography.bodyMedium, color = textColor)
|
||||
}
|
||||
|
@ -322,15 +339,11 @@ class FeedSettingsFragment : Fragment() {
|
|||
Row(Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.pref_auto_download_counting_played_title), style = MaterialTheme.typography.titleLarge, color = textColor)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
var checked by remember {
|
||||
mutableStateOf(feed?.preferences?.countingPlayed ?: true)
|
||||
}
|
||||
var checked by remember { mutableStateOf(feed?.preferences?.countingPlayed ?: true) }
|
||||
Switch(checked = checked, modifier = Modifier.height(24.dp),
|
||||
onCheckedChange = {
|
||||
checked = it
|
||||
feed = upsertBlk(feed!!) { f ->
|
||||
f.preferences?.countingPlayed = checked
|
||||
}
|
||||
feed = upsertBlk(feed!!) { f -> f.preferences?.countingPlayed = checked }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -341,14 +354,9 @@ class FeedSettingsFragment : Fragment() {
|
|||
Row(Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.episode_inclusive_filters_label), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
object : AutoDownloadFilterPrefDialog(requireContext(),
|
||||
feed?.preferences!!.autoDownloadFilter!!,
|
||||
1) {
|
||||
|
||||
object : AutoDownloadFilterPrefDialog(requireContext(), feed?.preferences!!.autoDownloadFilter!!, 1) {
|
||||
override fun onConfirmed(filter: FeedAutoDownloadFilter) {
|
||||
feed = upsertBlk(feed!!) {
|
||||
it.preferences?.autoDownloadFilter = filter
|
||||
}
|
||||
feed = upsertBlk(feed!!) { it.preferences?.autoDownloadFilter = filter }
|
||||
}
|
||||
}.show()
|
||||
})
|
||||
|
@ -361,14 +369,9 @@ class FeedSettingsFragment : Fragment() {
|
|||
Row(Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.episode_exclusive_filters_label), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
object : AutoDownloadFilterPrefDialog(requireContext(),
|
||||
feed?.preferences!!.autoDownloadFilter!!,
|
||||
-1) {
|
||||
|
||||
object : AutoDownloadFilterPrefDialog(requireContext(), feed?.preferences!!.autoDownloadFilter!!, -1) {
|
||||
override fun onConfirmed(filter: FeedAutoDownloadFilter) {
|
||||
feed = upsertBlk(feed!!) {
|
||||
it.preferences?.autoDownloadFilter = filter
|
||||
}
|
||||
feed = upsertBlk(feed!!) { it.preferences?.autoDownloadFilter = filter }
|
||||
}
|
||||
}.show()
|
||||
})
|
||||
|
@ -543,11 +546,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
AutoDownloadPolicy.entries.forEach { item ->
|
||||
Row(Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = (item == selectedOption),
|
||||
onCheckedChange = {
|
||||
Logd(TAG, "row clicked: $item $selectedOption")
|
||||
|
@ -577,18 +576,13 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
var newCache by remember { mutableStateOf((feed?.preferences?.autoDLMaxEpisodes ?: 1).toString()) }
|
||||
TextField(value = newCache, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) newCache = it },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
// visualTransformation = PositiveIntegerTransform(),
|
||||
label = { Text("Max episodes allowed") }
|
||||
)
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("Max episodes allowed") })
|
||||
Button(onClick = {
|
||||
if (newCache.isNotEmpty()) {
|
||||
feed = upsertBlk(feed!!) { it.preferences?.autoDLMaxEpisodes = newCache.toIntOrNull() ?: 1 }
|
||||
onDismiss()
|
||||
}
|
||||
}) {
|
||||
Text("Confirm")
|
||||
}
|
||||
}) { Text("Confirm") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -648,6 +642,90 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetAudioQuality(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) {
|
||||
var selected by remember {mutableStateOf(selectedOption)}
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FeedPreferences.AVQuality.entries.forEach { option ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = option.tag == selected,
|
||||
onCheckedChange = { isChecked ->
|
||||
selected = option.tag
|
||||
if (isChecked) Logd(TAG, "$option is checked")
|
||||
when (selected) {
|
||||
FeedPreferences.AVQuality.LOW.tag -> {
|
||||
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.LOW.code }
|
||||
onDismissRequest()
|
||||
}
|
||||
FeedPreferences.AVQuality.MEDIUM.tag -> {
|
||||
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.MEDIUM.code }
|
||||
onDismissRequest()
|
||||
}
|
||||
FeedPreferences.AVQuality.HIGH.tag -> {
|
||||
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.HIGH.code }
|
||||
onDismissRequest()
|
||||
}
|
||||
else -> {
|
||||
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.GLOBAL.code }
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(option.tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetVideoQuality(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) {
|
||||
var selected by remember {mutableStateOf(selectedOption)}
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FeedPreferences.AVQuality.entries.forEach { option ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = option.tag == selected,
|
||||
onCheckedChange = { isChecked ->
|
||||
selected = option.tag
|
||||
if (isChecked) Logd(TAG, "$option is checked")
|
||||
when (selected) {
|
||||
FeedPreferences.AVQuality.LOW.tag -> {
|
||||
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.LOW.code }
|
||||
onDismissRequest()
|
||||
}
|
||||
FeedPreferences.AVQuality.MEDIUM.tag -> {
|
||||
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.MEDIUM.code }
|
||||
onDismissRequest()
|
||||
}
|
||||
FeedPreferences.AVQuality.HIGH.tag -> {
|
||||
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.HIGH.code }
|
||||
onDismissRequest()
|
||||
}
|
||||
else -> {
|
||||
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.GLOBAL.code }
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
Text(option.tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthenticationDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
|
@ -669,9 +747,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Thread({ runOnce(requireContext(), feed) }, "RefreshAfterCredentialChange").start()
|
||||
onDismiss()
|
||||
}
|
||||
}) {
|
||||
Text("Confirm")
|
||||
}
|
||||
}) { Text("Confirm") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -685,19 +761,11 @@ class FeedSettingsFragment : Fragment() {
|
|||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
var intro by remember { mutableStateOf((feed?.preferences?.introSkip ?: 0).toString()) }
|
||||
TextField(value = intro,
|
||||
onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) intro = it },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
// visualTransformation = PositiveIntegerTransform(),
|
||||
label = { Text("Skip first (seconds)") }
|
||||
)
|
||||
TextField(value = intro, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) intro = it },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("Skip first (seconds)") })
|
||||
var ending by remember { mutableStateOf((feed?.preferences?.endingSkip ?: 0).toString()) }
|
||||
TextField(value = ending,
|
||||
onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) ending = it },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
// visualTransformation = PositiveIntegerTransform(),
|
||||
label = { Text("Skip last (seconds)") }
|
||||
)
|
||||
TextField(value = ending, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) ending = it },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("Skip last (seconds)") })
|
||||
Button(onClick = {
|
||||
if (intro.isNotEmpty() || ending.isNotEmpty()) {
|
||||
feed = upsertBlk(feed!!) {
|
||||
|
@ -706,9 +774,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
onDismiss()
|
||||
}
|
||||
}) {
|
||||
Text("Confirm")
|
||||
}
|
||||
}) { Text("Confirm") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -728,9 +794,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
val speed = feed?.preferences!!.playSpeed
|
||||
binding.useGlobalCheckbox.isChecked = speed == FeedPreferences.SPEED_USE_GLOBAL
|
||||
binding.seekBar.updateSpeed(if (speed == FeedPreferences.SPEED_USE_GLOBAL) 1f else speed)
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.playback_speed)
|
||||
.setView(binding.root)
|
||||
return MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.playback_speed).setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
val newSpeed = if (binding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL
|
||||
else binding.seekBar.currentSpeed
|
||||
|
@ -832,9 +896,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
protected abstract fun onConfirmed(filter: FeedAutoDownloadFilter)
|
||||
private fun toFilterString(words: List<String>?): String {
|
||||
val result = StringBuilder()
|
||||
for (word in words!!) {
|
||||
result.append("\"").append(word).append("\" ")
|
||||
}
|
||||
for (word in words!!) result.append("\"").append(word).append("\" ")
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -297,7 +297,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
feed.preferences!!.videoModePolicy = VideoMode.WINDOW_VIEW
|
||||
CustomFeedNameDialog(activity as Activity, feed).show()
|
||||
}
|
||||
R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext())
|
||||
R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext(), fullUpdate = true)
|
||||
R.id.toggle_grid_list -> useGrid = if (useGrid == null) !useGridLayout else !useGrid!!
|
||||
else -> return false
|
||||
}
|
||||
|
@ -961,7 +961,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
@Composable
|
||||
fun FilterDialog(filter: FeedFilter? = null, onDismissRequest: () -> Unit) {
|
||||
val filterValues: MutableSet<String> = mutableSetOf()
|
||||
val filterValues = remember { filter?.properties ?: mutableSetOf() }
|
||||
|
||||
fun onFilterChanged(newFilterValues: Set<String>) {
|
||||
feedsFilter = StringUtils.join(newFilterValues, ",")
|
||||
|
@ -975,7 +975,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
window.setGravity(Gravity.BOTTOM)
|
||||
window.setDimAmount(0f)
|
||||
}
|
||||
Surface(modifier = Modifier.fillMaxWidth().height(350.dp), color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp)) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||
|
|
|
@ -575,6 +575,10 @@
|
|||
<string name="pref_feed_skip">Auto skip</string>
|
||||
<string name="pref_feed_skip_sum">Skip introductions and ending credits.</string>
|
||||
<string name="associated">Associated</string>
|
||||
<string name="pref_feed_audio_quality">Audio quality</string>
|
||||
<string name="pref_feed_audio_quality_sum">Global generally equals high quality except when prefLowQualityMedia is set for metered network. Quality setting here takes precedence over the setting of prefLowQualityMedia for metered network.</string>
|
||||
<string name="pref_feed_video_quality">Video quality</string>
|
||||
<string name="pref_feed_video_quality_sum">Global equals low quality, Quality setting here takes precedence over the setting of prefLowQualityMedia for metered network.</string>
|
||||
<string name="pref_feed_associated_queue">Associated queue</string>
|
||||
<string name="pref_feed_associated_queue_sum">The queue to which epiosdes in the feed will added by default</string>
|
||||
<string name="pref_feed_skip_ending">Skip last</string>
|
||||
|
|
|
@ -4,6 +4,7 @@ import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
|||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.storage.model.RemoteMedia
|
||||
|
@ -11,7 +12,9 @@ import ac.mdiq.podcini.util.Logd
|
|||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import com.google.android.gms.cast.*
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
|
@ -19,6 +22,11 @@ import com.google.android.gms.cast.framework.CastState
|
|||
import com.google.android.gms.cast.framework.media.RemoteMediaClient
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlin.math.max
|
||||
|
@ -194,7 +202,7 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl
|
|||
*
|
||||
* @see .playMediaObject
|
||||
*/
|
||||
override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean, forceReset: Boolean) {
|
||||
override fun playMediaObject(playable: Playable, streaming: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean, forceReset: Boolean) {
|
||||
if (!CastUtils.isCastable(playable, castContext.sessionManager.currentCastSession)) {
|
||||
Logd(TAG, "media provided is not compatible with cast device")
|
||||
EventFlow.postEvent(FlowEvent.PlayerErrorEvent("Media not compatible with cast device"))
|
||||
|
@ -202,7 +210,7 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl
|
|||
do { nextPlayable = callback.getNextInQueue(nextPlayable)
|
||||
} while (nextPlayable != null && !CastUtils.isCastable(nextPlayable, castContext.sessionManager.currentCastSession))
|
||||
|
||||
if (nextPlayable != null) playMediaObject(nextPlayable, stream, startWhenPrepared, prepareImmediately, forceReset)
|
||||
if (nextPlayable != null) playMediaObject(nextPlayable, streaming, startWhenPrepared, prepareImmediately, forceReset)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -235,6 +243,53 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl
|
|||
this.mediaType = curMedia!!.getMediaType()
|
||||
this.startWhenPrepared.set(startWhenPrepared)
|
||||
setPlayerStatus(PlayerStatus.INITIALIZING, curMedia)
|
||||
|
||||
// val metadata = buildMetadata(curMedia!!)
|
||||
// try {
|
||||
// callback.ensureMediaInfoLoaded(curMedia!!)
|
||||
// callback.onMediaChanged(false)
|
||||
// setPlaybackParams(getCurrentPlaybackSpeed(curMedia), isSkipSilence)
|
||||
// CoroutineScope(Dispatchers.IO).launch {
|
||||
// when {
|
||||
// streaming -> {
|
||||
// val streamurl = curMedia!!.getStreamUrl()
|
||||
// if (streamurl != null) {
|
||||
// val media = curMedia
|
||||
// if (media is EpisodeMedia) {
|
||||
// mediaItem = null
|
||||
// mediaSource = null
|
||||
// setDataSource(metadata, media)
|
||||
//// val deferred = CoroutineScope(Dispatchers.IO).async { setDataSource(metadata, media) }
|
||||
//// if (startWhenPrepared) runBlocking { deferred.await() }
|
||||
//// val preferences = media.episodeOrFetch()?.feed?.preferences
|
||||
//// setDataSource(metadata, streamurl, preferences?.username, preferences?.password)
|
||||
// } else setDataSource(metadata, streamurl, null, null)
|
||||
// }
|
||||
// }
|
||||
// else -> {
|
||||
// val localMediaurl = curMedia!!.getLocalMediaUrl()
|
||||
//// File(localMediaurl).canRead() time consuming, leave it to MediaItem to handle
|
||||
//// if (!localMediaurl.isNullOrEmpty() && File(localMediaurl).canRead()) setDataSource(metadata, localMediaurl, null, null)
|
||||
// if (!localMediaurl.isNullOrEmpty()) setDataSource(metadata, localMediaurl, null, null)
|
||||
// else throw IOException("Unable to read local file $localMediaurl")
|
||||
// }
|
||||
// }
|
||||
// withContext(Dispatchers.Main) {
|
||||
// val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
// if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED, curMedia)
|
||||
// if (prepareImmediately) prepare()
|
||||
// }
|
||||
// }
|
||||
// } catch (e: IOException) {
|
||||
// e.printStackTrace()
|
||||
// setPlayerStatus(PlayerStatus.ERROR, null)
|
||||
// EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(e.localizedMessage ?: ""))
|
||||
// } catch (e: IllegalStateException) {
|
||||
// e.printStackTrace()
|
||||
// setPlayerStatus(PlayerStatus.ERROR, null)
|
||||
// EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(e.localizedMessage ?: ""))
|
||||
// } finally { }
|
||||
|
||||
callback.ensureMediaInfoLoaded(curMedia!!)
|
||||
callback.onMediaChanged(true)
|
||||
setPlayerStatus(PlayerStatus.INITIALIZED, curMedia)
|
||||
|
@ -266,7 +321,7 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl
|
|||
|
||||
override fun reinit() {
|
||||
Logd(TAG, "reinit() called")
|
||||
if (curMedia != null) playMediaObject(curMedia!!, stream = false, startWhenPrepared = startWhenPrepared.get(), prepareImmediately = false, true)
|
||||
if (curMedia != null) playMediaObject(curMedia!!, streaming = false, startWhenPrepared = startWhenPrepared.get(), prepareImmediately = false, true)
|
||||
else Logd(TAG, "Call to reinit was ignored: media was null")
|
||||
}
|
||||
|
||||
|
@ -342,7 +397,7 @@ class CastMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPl
|
|||
callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode)
|
||||
// setting media to null signals to playMediaObject() that we're taking care of post-playback processing
|
||||
curMedia = null
|
||||
playMediaObject(nextMedia, stream = true, startWhenPrepared = playNextEpisode, prepareImmediately = playNextEpisode, forceReset = false)
|
||||
playMediaObject(nextMedia, streaming = true, startWhenPrepared = playNextEpisode, prepareImmediately = playNextEpisode, forceReset = false)
|
||||
}
|
||||
}
|
||||
when {
|
||||
|
|
13
changelog.md
13
changelog.md
|
@ -1,3 +1,16 @@
|
|||
# 6.13.6
|
||||
|
||||
* created a fast update routine to improve performance
|
||||
* all updates (swipe or scheduled etc) run the fast update routine
|
||||
* full update (for which I don't see the reason) can be run from the menu of Subscriptions view
|
||||
* the full update routine is also enhanced for performance
|
||||
* enhanced youtube update routines to include infinite new episodes updates
|
||||
* added update routine for youtube channel tabs
|
||||
* added audio and video quality settings in Feed Preferences (Youtube feeds only): Global, Low, Medium, High
|
||||
* these settings take precedence over global situations
|
||||
* when Global is set, video is a lowest quality, and audio is at highest quality (except when prefLowQualityMedia is set for metered networks)
|
||||
* fixed mis-behavior of multi-selection filters
|
||||
|
||||
# 6.13.5
|
||||
|
||||
* hopefully fixed youtube playlists/podcasts no-updating issue
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
Version 6.13.6
|
||||
|
||||
* created a fast update routine to improve performance
|
||||
* all updates (swipe or scheduled etc) run the fast update routine
|
||||
* full update (for which I don't see the reason) can be run from the menu of Subscriptions view
|
||||
* the full update routine is also enhanced for performance
|
||||
* enhanced youtube update routines to include infinite new episodes updates
|
||||
* added update routine for youtube channel tabs
|
||||
* added audio and video quality settings in Feed Preferences (Youtube feeds only): Global, Low, Medium, High
|
||||
* these settings take precedence over global situations
|
||||
* when Global is set, video is a lowest quality, and audio is at highest quality (except when prefLowQualityMedia is set for metered networks)
|
||||
* fixed mis-behavior of multi-selection filters
|
|
@ -49,7 +49,6 @@ rxandroid = "3.0.2"
|
|||
rxjava = "2.2.21"
|
||||
rxjavaVersion = "3.1.8"
|
||||
searchpreference = "v2.5.0"
|
||||
#stream = "1.2.2"
|
||||
uiToolingPreview = "1.7.5"
|
||||
uiTooling = "1.7.5"
|
||||
viewpager2 = "1.1.0"
|
||||
|
@ -115,7 +114,6 @@ rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroi
|
|||
rxjava3-rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjavaVersion" }
|
||||
rxjava = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava" }
|
||||
searchpreference = { module = "com.github.ByteHamster:SearchPreference", version.ref = "searchpreference" }
|
||||
#stream = { module = "com.annimon:stream", version.ref = "stream" }
|
||||
vistaguide = { module = "com.github.XilinJia.vistaguide:VistaGuide", version.ref = "vistaguide" }
|
||||
desugar_jdk_libs_nio = { module = "com.android.tools:desugar_jdk_libs_nio", version.ref = "desugar_jdk_libs_nio" }
|
||||
wearable = { module = "com.google.android.wearable:wearable", version.ref = "wearable" }
|
||||
|
|
Loading…
Reference in New Issue