6.7.2 commit

This commit is contained in:
Xilin Jia 2024-09-19 22:43:15 +01:00
parent daeee36985
commit bea7ce74a7
16 changed files with 156 additions and 91 deletions

View File

@ -12,7 +12,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
[<img src="./images/external/getItf-droid.png" alt="F-Droid" height="50">](https://f-droid.org/packages/ac.mdiq.podcini.R/) [<img src="./images/external/getItf-droid.png" alt="F-Droid" height="50">](https://f-droid.org/packages/ac.mdiq.podcini.R/)
[<img src="./images/external/amazon.png" alt="Amazon" height="40">](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13) [<img src="./images/external/amazon.png" alt="Amazon" height="40">](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13)
#### Podcini.R 6.6 introduces the powerful feature of synthetic podcasts, enables the receiving/handling shared single media as well as playlist from Youtube and YT Music, for more see the Youtube section below or the changelogs. #### Podcini.R 6.6 introduces the powerful feature of synthetic podcasts, enables the receiving/handling shared single media as well as playlist and podcast from Youtube and YT Music, for more see the Youtube section below or the changelogs.
That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs) That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs)
#### Podcini.R version 6.5 as a major step forward brings YouTube channels in the app. They can be searched, received from share, subscribed and played from within Podcini. For more see the Youtube section below or the changelogs #### Podcini.R version 6.5 as a major step forward brings YouTube channels in the app. They can be searched, received from share, subscribed and played from within Podcini. For more see the Youtube section below or the changelogs
#### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions. #### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions.
@ -131,7 +131,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* Youtube channels can be searched in podcast search view, can also be shared from other apps (such as Youtube) to Podcini * Youtube channels can be searched in podcast search view, can also be shared from other apps (such as Youtube) to Podcini
* Youtube channels can be subscribed as normal podcasts * Youtube channels can be subscribed as normal podcasts
* Playlists on Youtube or Youtube Music can be shared to Podcini, and then can be subscribed in similar fashion as the channels * Playlists and podcasts on Youtube or Youtube Music can be shared to Podcini, and then can be subscribed in similar fashion as the channels
* Single media from Youtube or Youtube Music can also be shared from other apps, can be accepted as including video or audio only, are added to synthetic podcasts such as "Youtube Syndicate" * Single media from Youtube or Youtube Music can also be shared from other apps, can be accepted as including video or audio only, are added to synthetic podcasts such as "Youtube Syndicate"
* All the media from Youtube or Youtube Music can be played (only streamed) with video in fullscreen and in window modes or in audio only mode in the background * All the media from Youtube or Youtube Music can be played (only streamed) with video in fullscreen and in window modes or in audio only mode in the background
* These media are played with the lowest video quality and highest audio quality * These media are played with the lowest video quality and highest audio quality

View File

@ -19,8 +19,8 @@ composeCompiler {
android { android {
defaultConfig { defaultConfig {
minSdk 24 minSdk 24
compileSdk 34 compileSdk 35
targetSdk 34 targetSdk 35
kotlinOptions { kotlinOptions {
jvmTarget = '17' jvmTarget = '17'
@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests" testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020254 versionCode 3020255
versionName "6.7.1" versionName "6.7.2"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""
@ -174,12 +174,12 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.2' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.2'
implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6' implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
def composeBom = platform('androidx.compose:compose-bom:2024.09.01') def composeBom = platform('androidx.compose:compose-bom:2024.09.02')
implementation composeBom implementation composeBom
androidTestImplementation composeBom androidTestImplementation composeBom
implementation 'androidx.compose.material:material:1.7.1' implementation 'androidx.compose.material:material:1.7.2'
implementation 'androidx.compose.ui:ui-tooling-preview:1.7.1' implementation 'androidx.compose.ui:ui-tooling-preview:1.7.2'
debugImplementation 'androidx.compose.ui:ui-tooling:1.7.1' debugImplementation 'androidx.compose.ui:ui-tooling:1.7.2'
implementation 'androidx.activity:activity-compose:1.9.2' implementation 'androidx.activity:activity-compose:1.9.2'
implementation 'androidx.window:window:1.3.0' implementation 'androidx.window:window:1.3.0'
@ -187,7 +187,7 @@ dependencies {
implementation "androidx.core:core-ktx:1.13.1" implementation "androidx.core:core-ktx:1.13.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.5" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.6"
implementation "androidx.annotation:annotation:1.8.2" implementation "androidx.annotation:annotation:1.8.2"
implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
@ -205,7 +205,7 @@ dependencies {
implementation "androidx.work:work-runtime:2.9.1" implementation "androidx.work:work-runtime:2.9.1"
implementation "androidx.core:core-splashscreen:1.0.1" implementation "androidx.core:core-splashscreen:1.0.1"
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.webkit:webkit:1.11.0' implementation 'androidx.webkit:webkit:1.12.0'
implementation "com.google.android.material:material:1.12.0" implementation "com.google.android.material:material:1.12.0"

View File

@ -71,8 +71,8 @@ class DownloadRequest private constructor(
// of them from a Parcel (from an Intent extra to submit a request to DownloadService) will fail. // of them from a Parcel (from an Intent extra to submit a request to DownloadService) will fail.
// //
// see: https://stackoverflow.com/a/22926342 // see: https://stackoverflow.com/a/22926342
dest.writeString(nonNullString(username)) dest.writeString(username ?: "")
dest.writeString(nonNullString(password)) dest.writeString(password ?: "")
dest.writeByte(if ((mediaEnqueued)) 1.toByte() else 0) dest.writeByte(if ((mediaEnqueued)) 1.toByte() else 0)
dest.writeBundle(arguments) dest.writeBundle(arguments)
dest.writeByte(if (initiatedByUser) 1.toByte() else 0) dest.writeByte(if (initiatedByUser) 1.toByte() else 0)
@ -183,10 +183,6 @@ class DownloadRequest private constructor(
companion object { companion object {
const val REQUEST_ARG_PAGE_NR: String = "page" const val REQUEST_ARG_PAGE_NR: String = "page"
private fun nonNullString(str: String?): String {
return str ?: ""
}
private fun nullIfEmpty(str: String?): String? { private fun nullIfEmpty(str: String?): String? {
return if (str.isNullOrEmpty()) null else str return if (str.isNullOrEmpty()) null else str
} }

View File

@ -138,6 +138,7 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
downloadRequest.progressPercent = progressPercent downloadRequest.progressPercent = progressPercent
} }
} catch (e: IOException) { Log.e(TAG, Log.getStackTraceString(e)) } } catch (e: IOException) { Log.e(TAG, Log.getStackTraceString(e)) }
if (cancelled) onCancelled() if (cancelled) onCancelled()
else { else {
// check if size specified in the response header is the same as the size of the // check if size specified in the response header is the same as the size of the

View File

@ -181,7 +181,7 @@ object Episodes {
@UnstableApi @UnstableApi
fun deleteEpisodes(context: Context, episodes: List<Episode>) : Job { fun deleteEpisodes(context: Context, episodes: List<Episode>) : Job {
return runOnIOScope { return runOnIOScope {
val removedFromQueue: MutableList<Episode> = ArrayList() val removedFromQueue: MutableList<Episode> = mutableListOf()
val queueItems = curQueue.episodes.toMutableList() val queueItems = curQueue.episodes.toMutableList()
for (episode in episodes) { for (episode in episodes) {
if (queueItems.remove(episode)) removedFromQueue.add(episode) if (queueItems.remove(episode)) removedFromQueue.add(episode)

View File

@ -200,7 +200,7 @@ object Feeds {
fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? { fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? {
Logd(TAG, "updateFeed called") Logd(TAG, "updateFeed called")
var resultFeed: Feed? var resultFeed: Feed?
val unlistedItems: MutableList<Episode> = ArrayList() // val unlistedItems: MutableList<Episode> = ArrayList()
// Look up feed in the feedslist // Look up feed in the feedslist
val savedFeed = searchFeedByIdentifyingValueOrID(newFeed, true) val savedFeed = searchFeedByIdentifyingValueOrID(newFeed, true)
@ -212,7 +212,8 @@ object Feeds {
addNewFeedsSync(context, newFeed) addNewFeedsSync(context, newFeed)
// Update with default values that are set in database // Update with default values that are set in database
resultFeed = searchFeedByIdentifyingValueOrID(newFeed) resultFeed = searchFeedByIdentifyingValueOrID(newFeed)
if (removeUnlistedItems) runBlocking { deleteEpisodes(context, unlistedItems).join() } // TODO: This doesn't appear needed as unlistedItems is still empty
// if (removeUnlistedItems && unlistedItems.isNotEmpty()) runBlocking { deleteEpisodes(context, unlistedItems).join() }
} catch (e: InterruptedException) { e.printStackTrace() } catch (e: InterruptedException) { e.printStackTrace()
} catch (e: ExecutionException) { e.printStackTrace() } } catch (e: ExecutionException) { e.printStackTrace() }
return resultFeed return resultFeed
@ -226,7 +227,7 @@ object Feeds {
savedFeed.updateFromOther(newFeed) savedFeed.updateFromOther(newFeed)
} }
} else { } else {
Logd(TAG, "New feed has a higher page number.") Logd(TAG, "New feed has a higher page number: ${newFeed.nextPageLink}")
savedFeed.nextPageLink = newFeed.nextPageLink savedFeed.nextPageLink = newFeed.nextPageLink
} }
val priorMostRecent = savedFeed.mostRecentItem val priorMostRecent = savedFeed.mostRecentItem
@ -240,9 +241,9 @@ object Feeds {
// Look for new or updated Items // Look for new or updated Items
for (idx in newFeed.episodes.indices) { for (idx in newFeed.episodes.indices) {
val episode = newFeed.episodes[idx] val episode = newFeed.episodes[idx]
var oldItem = savedFeedAssistant.searchEpisodeByIdentifyingValue(episode) var oldItem = savedFeedAssistant.getEpisodeByIdentifyingValue(episode)
if (!newFeed.isLocalFeed && oldItem == null) { if (!newFeed.isLocalFeed && oldItem == null) {
oldItem = savedFeedAssistant.searchEpisodeGuessDuplicate(episode) oldItem = savedFeedAssistant.guessDuplicate(episode)
if (oldItem != null) { if (oldItem != null) {
Logd(TAG, "Repaired duplicate: $oldItem, $episode") Logd(TAG, "Repaired duplicate: $oldItem, $episode")
addDownloadStatus(DownloadResult(savedFeed.id, addDownloadStatus(DownloadResult(savedFeed.id,
@ -257,6 +258,7 @@ object Feeds {
${EpisodeAssistant.duplicateEpisodeDetails(episode)} ${EpisodeAssistant.duplicateEpisodeDetails(episode)}
""".trimIndent())) """.trimIndent()))
oldItem.identifier = episode.identifier oldItem.identifier = episode.identifier
// queue for syncing with server
if (isProviderConnected && oldItem.isPlayed() && oldItem.media != null) { if (isProviderConnected && oldItem.isPlayed() && oldItem.media != null) {
val durs = oldItem.media!!.getDuration() / 1000 val durs = oldItem.media!!.getDuration() / 1000
val action = EpisodeAction.Builder(oldItem, EpisodeAction.PLAY) val action = EpisodeAction.Builder(oldItem, EpisodeAction.PLAY)
@ -295,12 +297,13 @@ object Feeds {
} }
savedFeedAssistant.clear() savedFeedAssistant.clear()
val unlistedItems: MutableList<Episode> = ArrayList()
// identify episodes to be removed // identify episodes to be removed
if (removeUnlistedItems) { if (removeUnlistedItems) {
val it = savedFeed.episodes.toMutableList().iterator() val it = savedFeed.episodes.toMutableList().iterator()
while (it.hasNext()) { while (it.hasNext()) {
val feedItem = it.next() val feedItem = it.next()
if (newFeedAssistant.searchEpisodeByIdentifyingValue(feedItem) == null) { if (newFeedAssistant.getEpisodeByIdentifyingValue(feedItem) == null) {
unlistedItems.add(feedItem) unlistedItems.add(feedItem)
it.remove() it.remove()
} }
@ -315,7 +318,7 @@ object Feeds {
resultFeed = savedFeed resultFeed = savedFeed
try { try {
upsertBlk(savedFeed) {} upsertBlk(savedFeed) {}
if (removeUnlistedItems) runBlocking { deleteEpisodes(context, unlistedItems).join() } if (removeUnlistedItems && unlistedItems.isNotEmpty()) runBlocking { deleteEpisodes(context, unlistedItems).join() }
} catch (e: InterruptedException) { e.printStackTrace() } catch (e: InterruptedException) { e.printStackTrace()
} catch (e: ExecutionException) { e.printStackTrace() } } catch (e: ExecutionException) { e.printStackTrace() }
return resultFeed return resultFeed
@ -461,7 +464,7 @@ object Feeds {
} }
// savedFeedId == 0L means saved feed // savedFeedId == 0L means saved feed
class FeedAssistant(val feed: Feed, val savedFeedId: Long = 0L) { class FeedAssistant(val feed: Feed, private val savedFeedId: Long = 0L) {
val map = mutableMapOf<String, Episode>() val map = mutableMapOf<String, Episode>()
val tag: String = if (savedFeedId == 0L) "Saved feed" else "New feed" val tag: String = if (savedFeedId == 0L) "Saved feed" else "New feed"
@ -538,10 +541,10 @@ object Feeds {
${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)} ${EpisodeAssistant.duplicateEpisodeDetails(possibleDuplicate)}
""".trimIndent())) """.trimIndent()))
} }
fun searchEpisodeByIdentifyingValue(item: Episode): Episode? { fun getEpisodeByIdentifyingValue(item: Episode): Episode? {
return map[item.identifyingValue] return map[item.identifyingValue]
} }
fun searchEpisodeGuessDuplicate(item: Episode): Episode? { fun guessDuplicate(item: Episode): Episode? {
var episode = map[item.identifier] var episode = map[item.identifier]
if (episode != null) return episode if (episode != null) return episode
val url = item.media?.getStreamUrl() val url = item.media?.getStreamUrl()

View File

@ -84,6 +84,7 @@ class Feed : RealmObject {
var preferences: FeedPreferences? = null var preferences: FeedPreferences? = null
// TODO: this might not be needed
var measures: FeedMeasures? = null var measures: FeedMeasures? = null
var hasVideoMedia: Boolean = false var hasVideoMedia: Boolean = false
@ -126,15 +127,6 @@ class Feed : RealmObject {
preferences?.sortOrderCode = value.code preferences?.sortOrderCode = value.code
} }
// @Ignore
// var sortOrderAux: EpisodeSortOrder? = null
// get() = fromCode(preferences?.sortOrderAuxCode ?: 0)
// set(value) {
// if (value == null) return
// field = value
// preferences?.sortOrderAuxCode = value.code
// }
@Ignore @Ignore
val mostRecentItem: Episode? 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()

View File

@ -3,6 +3,7 @@ package ac.mdiq.podcini.storage.model
import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.EmbeddedRealmObject
import io.realm.kotlin.types.annotations.Index import io.realm.kotlin.types.annotations.Index
// TODO: this might not be needed
class FeedMeasures : EmbeddedRealmObject { class FeedMeasures : EmbeddedRealmObject {
@Index @Index
var feedID: Long = 0L var feedID: Long = 0L

View File

@ -97,7 +97,7 @@ import java.util.concurrent.Semaphore
_binding = FeedItemListFragmentBinding.inflate(inflater) _binding = FeedItemListFragmentBinding.inflate(inflater)
_dialBinding = MultiSelectSpeedDialBinding.bind(binding.root) _dialBinding = MultiSelectSpeedDialBinding.bind(binding.root)
binding.toolbar.inflateMenu(R.menu.feedlist) binding.toolbar.inflateMenu(R.menu.feed_episodes)
binding.toolbar.setOnMenuItemClickListener(this) binding.toolbar.setOnMenuItemClickListener(this)
binding.toolbar.setOnLongClickListener { binding.toolbar.setOnLongClickListener {
binding.recyclerView.scrollToPosition(5) binding.recyclerView.scrollToPosition(5)
@ -132,7 +132,7 @@ import java.util.concurrent.Semaphore
val iconTintManager: ToolbarIconTintManager = object : ToolbarIconTintManager( val iconTintManager: ToolbarIconTintManager = object : ToolbarIconTintManager(
requireContext(), binding.toolbar, binding.collapsingToolbar) { requireContext(), binding.toolbar, binding.collapsingToolbar) {
override fun doTint(themedContext: Context) { override fun doTint(themedContext: Context) {
binding.toolbar.menu.findItem(R.id.refresh_item).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_refresh)) binding.toolbar.menu.findItem(R.id.refresh_feed).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_refresh))
binding.toolbar.menu.findItem(R.id.action_search).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_search)) binding.toolbar.menu.findItem(R.id.action_search).setIcon(AppCompatResources.getDrawable(themedContext, R.drawable.ic_search))
} }
} }
@ -268,7 +268,7 @@ import java.util.concurrent.Semaphore
binding.toolbar.menu.findItem(R.id.visit_website_item).setVisible(feed!!.link != null) binding.toolbar.menu.findItem(R.id.visit_website_item).setVisible(feed!!.link != null)
binding.toolbar.menu.findItem(R.id.refresh_complete_item).setVisible(feed!!.isPaged) binding.toolbar.menu.findItem(R.id.refresh_complete_item).setVisible(feed!!.isPaged)
if (StringUtils.isBlank(feed!!.link)) binding.toolbar.menu.findItem(R.id.visit_website_item).setVisible(false) if (StringUtils.isBlank(feed!!.link)) binding.toolbar.menu.findItem(R.id.visit_website_item).setVisible(false)
if (feed!!.isLocalFeed) binding.toolbar.menu.findItem(R.id.share_item).setVisible(false) if (feed!!.isLocalFeed) binding.toolbar.menu.findItem(R.id.share_feed).setVisible(false)
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
@ -285,8 +285,8 @@ import java.util.concurrent.Semaphore
} }
when (item.itemId) { when (item.itemId) {
R.id.visit_website_item -> if (feed!!.link != null) IntentUtils.openInBrowser(requireContext(), feed!!.link!!) R.id.visit_website_item -> if (feed!!.link != null) IntentUtils.openInBrowser(requireContext(), feed!!.link!!)
R.id.share_item -> ShareUtils.shareFeedLink(requireContext(), feed!!) R.id.share_feed -> ShareUtils.shareFeedLink(requireContext(), feed!!)
R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext(), feed) R.id.refresh_feed -> FeedUpdateManager.runOnceOrAsk(requireContext(), feed)
R.id.refresh_complete_item -> { R.id.refresh_complete_item -> {
Thread { Thread {
try { try {
@ -295,21 +295,14 @@ import java.util.concurrent.Semaphore
it.nextPageLink = it.downloadUrl it.nextPageLink = it.downloadUrl
it.pageNr = 0 it.pageNr = 0
} }
// val feed_ = unmanaged(feed!!)
// feed_.nextPageLink = feed_.downloadUrl
// feed_.pageNr = 0
// upsertBlk(feed_) {}
FeedUpdateManager.runOnce(requireContext(), feed_) FeedUpdateManager.runOnce(requireContext(), feed_)
} }
} catch (e: ExecutionException) { } catch (e: ExecutionException) { throw RuntimeException(e)
throw RuntimeException(e) } catch (e: InterruptedException) { throw RuntimeException(e) }
} catch (e: InterruptedException) {
throw RuntimeException(e)
}
}.start() }.start()
} }
R.id.sort_items -> SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog") R.id.sort_items -> SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog")
R.id.rename_item -> CustomFeedNameDialog(activity as Activity, feed!!).show() R.id.rename_feed -> CustomFeedNameDialog(activity as Activity, feed!!).show()
R.id.remove_feed -> { R.id.remove_feed -> {
RemoveFeedDialog.show(requireContext(), feed!!) { RemoveFeedDialog.show(requireContext(), feed!!) {
(activity as MainActivity).loadFragment(UserPreferences.defaultPage, null) (activity as MainActivity).loadFragment(UserPreferences.defaultPage, null)

View File

@ -6,11 +6,13 @@ import ac.mdiq.podcini.databinding.FeedinfoBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
import ac.mdiq.podcini.net.utils.HtmlToPlainText import ac.mdiq.podcini.net.utils.HtmlToPlainText
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.Feeds.updateFeed import ac.mdiq.podcini.storage.database.Feeds.updateFeed
import ac.mdiq.podcini.storage.database.Feeds.updateFeedDownloadURL import ac.mdiq.podcini.storage.database.Feeds.updateFeedDownloadURL
import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.FeedFunding import ac.mdiq.podcini.storage.model.FeedFunding
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
import ac.mdiq.podcini.ui.statistics.FeedStatisticsFragment import ac.mdiq.podcini.ui.statistics.FeedStatisticsFragment
import ac.mdiq.podcini.ui.statistics.StatisticsFragment import ac.mdiq.podcini.ui.statistics.StatisticsFragment
import ac.mdiq.podcini.ui.utils.ToolbarIconTintManager import ac.mdiq.podcini.ui.utils.ToolbarIconTintManager
@ -248,6 +250,13 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
}.show() }.show()
} }
R.id.remove_feed -> {
RemoveFeedDialog.show(requireContext(), feed) {
(activity as MainActivity).loadFragment(UserPreferences.defaultPage, null)
// Make sure fragment is hidden before actually starting to delete
requireActivity().supportFragmentManager.executePendingTransactions()
}
}
else -> return false else -> return false
} }
return true return true
@ -255,10 +264,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@UnstableApi private fun addLocalFolderResult(uri: Uri?) { @UnstableApi private fun addLocalFolderResult(uri: Uri?) {
if (uri == null) return if (uri == null) return
reconnectLocalFolder(uri) // reconnectLocalFolder(uri)
}
@UnstableApi private fun reconnectLocalFolder(uri: Uri) {
lifecycleScope.launch { lifecycleScope.launch {
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -279,6 +285,27 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
} }
// @UnstableApi private fun reconnectLocalFolder(uri: Uri) {
// lifecycleScope.launch {
// try {
// withContext(Dispatchers.IO) {
// requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
// val documentFile = DocumentFile.fromTreeUri(requireContext(), uri)
// requireNotNull(documentFile) { "Unable to retrieve document tree" }
// feed.downloadUrl = Feed.PREFIX_LOCAL_FOLDER + uri.toString()
// updateFeed(requireContext(), feed, true)
// }
// withContext(Dispatchers.Main) {
// (activity as MainActivity).showSnackbarAbovePlayer(string.ok, Snackbar.LENGTH_SHORT)
// }
// } catch (e: Throwable) {
// withContext(Dispatchers.Main) {
// (activity as MainActivity).showSnackbarAbovePlayer(e.localizedMessage, Snackbar.LENGTH_LONG)
// }
// }
// }
// }
private var eventSink: Job? = null private var eventSink: Job? = null
private fun cancelFlowEvents() { private fun cancelFlowEvents() {
eventSink?.cancel() eventSink?.cancel()
@ -323,11 +350,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
runBlocking { updateFeedDownloadURL(original, updated).join() } runBlocking { updateFeedDownloadURL(original, updated).join() }
feed.downloadUrl = updated feed.downloadUrl = updated
runOnce(activityRef.get()!!, feed) runOnce(activityRef.get()!!, feed)
} catch (e: ExecutionException) { } catch (e: ExecutionException) { throw RuntimeException(e)
throw RuntimeException(e) } catch (e: InterruptedException) { throw RuntimeException(e) }
} catch (e: InterruptedException) {
throw RuntimeException(e)
}
} }
@UnstableApi private fun showConfirmAlertDialog(url: String) { @UnstableApi private fun showConfirmAlertDialog(url: String) {
val activity = activityRef.get() val activity = activityRef.get()

View File

@ -5,9 +5,9 @@ import ac.mdiq.podcini.databinding.EditTextDialogBinding
import ac.mdiq.podcini.databinding.OnlineFeedviewFragmentBinding import ac.mdiq.podcini.databinding.OnlineFeedviewFragmentBinding
import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.download.service.Downloader import ac.mdiq.podcini.net.download.service.Downloader
import ac.mdiq.podcini.net.download.service.HttpDownloader import ac.mdiq.podcini.net.download.service.HttpDownloader
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
@ -24,8 +24,6 @@ import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath
import ac.mdiq.podcini.storage.utils.FilesUtils.getFeedfileName import ac.mdiq.podcini.storage.utils.FilesUtils.getFeedfileName
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter.EpisodeInfoFragment
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter.EpisodeInfoFragment.Companion
import ac.mdiq.podcini.ui.dialog.AuthenticationDialog import ac.mdiq.podcini.ui.dialog.AuthenticationDialog
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.EventFlow
@ -38,7 +36,6 @@ import ac.mdiq.vista.extractor.channel.ChannelInfo
import ac.mdiq.vista.extractor.channel.tabs.ChannelTabInfo import ac.mdiq.vista.extractor.channel.tabs.ChannelTabInfo
import ac.mdiq.vista.extractor.exceptions.ExtractionException import ac.mdiq.vista.extractor.exceptions.ExtractionException
import ac.mdiq.vista.extractor.playlist.PlaylistInfo import ac.mdiq.vista.extractor.playlist.PlaylistInfo
import ac.mdiq.vista.extractor.stream.StreamInfo
import ac.mdiq.vista.extractor.stream.StreamInfoItem import ac.mdiq.vista.extractor.stream.StreamInfoItem
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
@ -239,7 +236,10 @@ class OnlineFeedFragment : Fragment() {
private fun htmlOrXml(url: String): String? { private fun htmlOrXml(url: String): String? {
val connection = URL(url).openConnection() as HttpURLConnection val connection = URL(url).openConnection() as HttpURLConnection
var type: String? = null var type: String? = null
try { type = connection.contentType } catch (e: IOException) { Log.e(TAG, "Error connecting to URL", e) } finally { connection.disconnect() } try { type = connection.contentType } catch (e: IOException) {
Log.e(TAG, "Error connecting to URL", e)
showErrorDialog(e.message, "")
} finally { connection.disconnect() }
if (type == null) return null if (type == null) return null
Logd(TAG, "connection type: $type") Logd(TAG, "connection type: $type")
return when { return when {
@ -273,8 +273,7 @@ class OnlineFeedFragment : Fragment() {
var infoItems = playlistInfo.relatedItems var infoItems = playlistInfo.relatedItems
var nextPage = playlistInfo.nextPage var nextPage = playlistInfo.nextPage
Logd(TAG, "infoItems: ${infoItems.size}") Logd(TAG, "infoItems: ${infoItems.size}")
var i = 0 while (infoItems.isNotEmpty()) {
while (infoItems.isNotEmpty() && i++ < 2) {
for (r in infoItems) { for (r in infoItems) {
Logd(TAG, "startFeedBuilding relatedItem: $r") Logd(TAG, "startFeedBuilding relatedItem: $r")
if (r.infoType != InfoItem.InfoType.STREAM) continue if (r.infoType != InfoItem.InfoType.STREAM) continue
@ -283,18 +282,27 @@ class OnlineFeedFragment : Fragment() {
e.feedId = feed_.id e.feedId = feed_.id
eList.add(e) eList.add(e)
} }
if (nextPage == null) break if (nextPage == null || eList.size > 500) break
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break try {
nextPage = page.nextPage val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
infoItems = page.items nextPage = page.nextPage
Logd(TAG, "more infoItems: ${infoItems.size}") infoItems = page.items
Logd(TAG, "more infoItems: ${infoItems.size}")
} catch (e: Throwable) {
Logd(TAG, "PlaylistInfo.getMoreItems error: ${e.message}")
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
break
}
} }
feed_.episodes = eList feed_.episodes = eList
withContext(Dispatchers.Main) { showFeedInformation(feed_, mapOf()) } withContext(Dispatchers.Main) { showFeedInformation(feed_, mapOf()) }
} else { } else {
val channelInfo = ChannelInfo.getInfo(service, url) val channelInfo = ChannelInfo.getInfo(service, url)
Logd(TAG, "startFeedBuilding result: $channelInfo ${channelInfo.tabs.size}") Logd(TAG, "startFeedBuilding result: $channelInfo ${channelInfo.tabs.size}")
if (channelInfo.tabs.isEmpty()) return@launch if (channelInfo.tabs.isEmpty()) {
withContext(Dispatchers.Main) { showErrorDialog("Channel is empty", "") }
return@launch
}
try { try {
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first()) val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first())
Logd(TAG, "startFeedBuilding result1: $channelTabInfo ${channelTabInfo.relatedItems.size}") Logd(TAG, "startFeedBuilding result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
@ -306,8 +314,7 @@ class OnlineFeedFragment : Fragment() {
var infoItems = channelTabInfo.relatedItems var infoItems = channelTabInfo.relatedItems
var nextPage = channelTabInfo.nextPage var nextPage = channelTabInfo.nextPage
Logd(TAG, "infoItems: ${infoItems.size}") Logd(TAG, "infoItems: ${infoItems.size}")
var i = 0 while (infoItems.isNotEmpty()) {
while (infoItems.isNotEmpty() && i++ < 2) {
for (r in infoItems) { for (r in infoItems) {
Logd(TAG, "startFeedBuilding relatedItem: $r") Logd(TAG, "startFeedBuilding relatedItem: $r")
if (r.infoType != InfoItem.InfoType.STREAM) continue if (r.infoType != InfoItem.InfoType.STREAM) continue
@ -316,17 +323,29 @@ class OnlineFeedFragment : Fragment() {
e.feedId = feed_.id e.feedId = feed_.id
eList.add(e) eList.add(e)
} }
if (nextPage == null) break if (nextPage == null || eList.size > 200) break
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage) try {
nextPage = page.nextPage val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
infoItems = page.items nextPage = page.nextPage
Logd(TAG, "more infoItems: ${infoItems.size}") infoItems = page.items
Logd(TAG, "more infoItems: ${infoItems.size}")
} catch (e: Throwable) {
Logd(TAG, "ChannelTabInfo.getMoreItems error: ${e.message}")
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
break
}
} }
feed_.episodes = eList feed_.episodes = eList
withContext(Dispatchers.Main) { showFeedInformation(feed_, mapOf()) } withContext(Dispatchers.Main) { showFeedInformation(feed_, mapOf()) }
} catch (e: Throwable) { Logd(TAG, "startFeedBuilding error1 ${e.message}") } } catch (e: Throwable) {
Logd(TAG, "startFeedBuilding error1 ${e.message}")
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
}
} }
} catch (e: Throwable) { Logd(TAG, "startFeedBuilding error ${e.message}") } } catch (e: Throwable) {
Logd(TAG, "startFeedBuilding error ${e.message}")
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
}
} }
return return
} }
@ -347,6 +366,7 @@ class OnlineFeedFragment : Fragment() {
"XML" -> {} "XML" -> {}
else -> { else -> {
Log.e(TAG, "unknown url type $urlType") Log.e(TAG, "unknown url type $urlType")
showErrorDialog("unknown url type $urlType", "")
return return
} }
} }
@ -388,7 +408,10 @@ class OnlineFeedFragment : Fragment() {
} }
} }
} }
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } } catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
}
} }
} }
@ -429,7 +452,10 @@ class OnlineFeedFragment : Fragment() {
this@OnlineFeedFragment.feeds = feeds this@OnlineFeedFragment.feeds = feeds
handleUpdatedFeedStatus() handleUpdatedFeedStatus()
} }
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } } catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
}
} }
} }

View File

@ -16,7 +16,7 @@
custom:showAsAction="always"> custom:showAsAction="always">
</item> </item>
<item <item
android:id="@+id/refresh_item" android:id="@+id/refresh_feed"
android:menuCategory="container" android:menuCategory="container"
android:title="@string/refresh_label" android:title="@string/refresh_label"
custom:showAsAction="never"> custom:showAsAction="never">
@ -44,13 +44,13 @@
</item> </item>
<item <item
android:id="@+id/share_item" android:id="@+id/share_feed"
android:menuCategory="container" android:menuCategory="container"
custom:showAsAction="never" custom:showAsAction="never"
android:title="@string/share_label" /> android:title="@string/share_label" />
<item <item
android:id="@+id/rename_item" android:id="@+id/rename_feed"
android:menuCategory="container" android:menuCategory="container"
android:title="@string/rename_feed_label" android:title="@string/rename_feed_label"
custom:showAsAction="never" /> custom:showAsAction="never" />

View File

@ -21,4 +21,12 @@
<item <item
android:id="@+id/edit_feed_url_item" android:id="@+id/edit_feed_url_item"
android:title="@string/edit_url_menu" /> android:title="@string/edit_url_menu" />
<item
android:id="@+id/remove_feed"
android:icon="@drawable/ic_delete"
android:menuCategory="container"
android:title="@string/remove_feed_label"
android:visible="true"
custom:showAsAction="collapseActionView">
</item>
</menu> </menu>

View File

@ -153,7 +153,7 @@
<string name="feed_auto_download_newer">Newest unplayed</string> <string name="feed_auto_download_newer">Newest unplayed</string>
<string name="feed_auto_download_older">Oldest unplayed</string> <string name="feed_auto_download_older">Oldest unplayed</string>
<string name="put_in_queue_label">Add to queue...</string> <string name="put_in_queue_label">Add to queue</string>
<string name="remove_from_other_queues">Remove from other queues</string> <string name="remove_from_other_queues">Remove from other queues</string>
<string name="feed_new_episodes_action_nothing">Nothing</string> <string name="feed_new_episodes_action_nothing">Nothing</string>
@ -363,7 +363,7 @@
<string name="queue_locked">Queue locked</string> <string name="queue_locked">Queue locked</string>
<string name="queue_unlocked">Queue unlocked</string> <string name="queue_unlocked">Queue unlocked</string>
<string name="queue_lock_warning">If you lock the queue, you can no longer swipe or reorder episodes.</string> <string name="queue_lock_warning">If you lock the queue, you can no longer swipe or reorder episodes.</string>
<string name="switch_queue">Switch queue</string> <string name="switch_queue">Switch active queue</string>
<string name="open_queue">Open queue</string> <string name="open_queue">Open queue</string>
<string name="checkbox_do_not_show_again">Do not show again</string> <string name="checkbox_do_not_show_again">Do not show again</string>
<string name="clear_queue_label">Clear queue</string> <string name="clear_queue_label">Clear queue</string>

View File

@ -1,3 +1,14 @@
# 6.7.2
* added menu item for removing feed in FeedInfo view
* menu item "Switch queue" is changed to "Switch active queue"
* Youtube and YT Music podcasts can be shared to Podcini
* initial max number of loaded items for Youtube and YT Music playlist and podcast is set 500
* initial max number of loaded items for Youtube channel is set 500
* added some error dialogs when handling shared links
* updated some dependencies including Compose
* compile and target SDK's are upped to 35
# 6.7.1 # 6.7.1
* ensured duplicate episodes are removed from secondary checking during refresh * ensured duplicate episodes are removed from secondary checking during refresh

View File

@ -0,0 +1,10 @@
Version 6.7.2:
* added menu item for removing feed in FeedInfo view
* menu item "Switch queue" is changed to "Switch active queue"
* Youtube and YT Music podcasts can be shared to Podcini
* initial max number of loaded items for Youtube and YT Music playlist and podcast is set 500
* initial max number of loaded items for Youtube channel is set 500
* added some error dialogs when handling shared links
* updated some dependencies including Compose
* compile and target SDK's are upped to 35