6.13.5 commit
This commit is contained in:
parent
2d614ad5d1
commit
a0d6557de2
|
@ -12,11 +12,12 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
|
||||||
[<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.10 allows creating synthetic podcast and shelving any episdes to any synthetic podcasts
|
#### Podcini.R 6.10 allows creating synthetic podcast and shelving any episdes to any synthetic podcasts
|
||||||
#### Podcini.R version 6.5 as a major step forward brings YouTube contents in the app. Channels can be searched, received from share, subscribed. Podcasts, playlists as well as single media from Youtube and YT Music can be shared to Podcini. For more see the Youtube section below or the changelogs
|
#### Podcini.R 6.5 as a major step forward brings YouTube contents in the app. Channels can be searched, received from share, subscribed. Podcasts, playlists as well as single media from Youtube and YT Music can be shared to Podcini. 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)
|
||||||
#### 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.
|
||||||
#### If you need to cast to an external speaker, you should install the "play" apk, not the "free" apk, that's about the difference between the two.
|
#### If you need to cast to an external speaker, you should install the "play" apk, not the "free" apk, that's about the difference between the two.
|
||||||
#### Since version 6.11.5, Podcini.R is back to be built to target SDK 35 (Android 15), but requests for permission for unrestricted background activities for uninterrupted background play of a playlist. For more see [this issue](https://github.com/XilinJia/Podcini/issues/88)
|
#### Since version 6.11.5, Podcini.R is back to be built to target SDK 35 (Android 15), but requests for permission for unrestricted background activities for uninterrupted background play of a playlist. For more see [this issue](https://github.com/XilinJia/Podcini/issues/88)
|
||||||
|
#### If you intend to sync with through a server, be cautious as it's not well tested with Podcini. Welcome any ideas and contribution on this.
|
||||||
#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions.
|
#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions.
|
||||||
|
|
||||||
This project was developed from a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
|
This project was developed from a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
|
||||||
|
@ -141,6 +142,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
|
||||||
|
* When subscribing to a Youtube channel, tabs can be chosen to form separate podcasts
|
||||||
* Playlists and podcasts 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
|
||||||
|
|
|
@ -26,8 +26,8 @@ android {
|
||||||
vectorDrawables.useSupportLibrary false
|
vectorDrawables.useSupportLibrary false
|
||||||
vectorDrawables.generatedDensities = []
|
vectorDrawables.generatedDensities = []
|
||||||
|
|
||||||
versionCode 3020291
|
versionCode 3020292
|
||||||
versionName "6.13.4"
|
versionName "6.13.5"
|
||||||
|
|
||||||
applicationId "ac.mdiq.podcini.R"
|
applicationId "ac.mdiq.podcini.R"
|
||||||
def commit = ""
|
def commit = ""
|
||||||
|
|
|
@ -18,11 +18,22 @@ import ac.mdiq.vista.extractor.InfoItem
|
||||||
import ac.mdiq.vista.extractor.Vista
|
import ac.mdiq.vista.extractor.Vista
|
||||||
import ac.mdiq.vista.extractor.channel.ChannelInfo
|
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.playlist.PlaylistInfo
|
import ac.mdiq.vista.extractor.playlist.PlaylistInfo
|
||||||
import ac.mdiq.vista.extractor.stream.StreamInfoItem
|
import ac.mdiq.vista.extractor.stream.StreamInfoItem
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import io.realm.kotlin.ext.realmListOf
|
import io.realm.kotlin.ext.realmListOf
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
@ -34,17 +45,88 @@ import java.net.URL
|
||||||
class FeedBuilder(val context: Context, val showError: (String?, String)->Unit) {
|
class FeedBuilder(val context: Context, val showError: (String?, String)->Unit) {
|
||||||
private val TAG = "DirectSubscribe"
|
private val TAG = "DirectSubscribe"
|
||||||
|
|
||||||
|
private val service by lazy { Vista.getService("YouTube") }
|
||||||
|
private val ytTabsMap: MutableMap<Int, String> = mutableMapOf()
|
||||||
|
private lateinit var channelInfo: ChannelInfo
|
||||||
|
|
||||||
var feedSource: String = ""
|
var feedSource: String = ""
|
||||||
var selectedDownloadUrl: String? = null
|
var selectedDownloadUrl: String? = null
|
||||||
private var downloader: Downloader? = null
|
private var downloader: Downloader? = null
|
||||||
|
|
||||||
fun startFeedBuilding(url: String, username: String?, password: String?, handleFeed: (Feed, Map<String, String>)->Unit) {
|
private var urlInit: String = ""
|
||||||
if (feedSource == "VistaGuide" || url.contains("youtube.com")) {
|
|
||||||
feedSource = "VistaGuide"
|
fun isYoutube(url: String): Boolean {
|
||||||
|
urlInit = url
|
||||||
|
val isit = (feedSource == "VistaGuide" || url.contains("youtube.com"))
|
||||||
|
if (isit) feedSource = "VistaGuide"
|
||||||
|
return isit
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isYoutubeChannel(): Boolean {
|
||||||
|
val uURL = URL(urlInit)
|
||||||
|
return !uURL.path.startsWith("/playlist")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun youtubeChannelValidTabs(): Int {
|
||||||
|
channelInfo = ChannelInfo.getInfo(service, urlInit)
|
||||||
|
var count = 0
|
||||||
|
for (i in channelInfo.tabs.indices) {
|
||||||
|
val t = channelInfo.tabs[i]
|
||||||
|
var url = t.url
|
||||||
|
Logd(TAG, "url: $url ${t.originalUrl} ${t.baseUrl}")
|
||||||
|
if (!url.startsWith("http")) url = urlInit + url
|
||||||
|
val uURL = URL(url)
|
||||||
|
val urlEnd = uURL.path.split("/").last()
|
||||||
|
if (urlEnd != "playlists" && urlEnd != "shorts") count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfirmYTChannelTabsDialog(onDismissRequest: () -> Unit, handleFeed: (Feed, Map<String, String>)->Unit) {
|
||||||
|
val textColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
Column {
|
||||||
|
Text(text = stringResource(R.string.choose_tab), color = textColor, style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 10.dp, bottom = 10.dp))
|
||||||
|
val selectedId = remember { mutableStateOf<Int?>(null) }
|
||||||
|
for (i in channelInfo.tabs.indices) {
|
||||||
|
val t = channelInfo.tabs[i]
|
||||||
|
var url = t.url
|
||||||
|
Logd(TAG, "url: $url ${t.originalUrl} ${t.baseUrl}")
|
||||||
|
if (!url.startsWith("http")) url = urlInit + url
|
||||||
|
val uURL = URL(url)
|
||||||
|
val urlEnd = uURL.path.split("/").last()
|
||||||
|
if (urlEnd != "playlists" && urlEnd != "shorts") Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 30.dp)) {
|
||||||
|
var checked by remember { mutableStateOf(false) }
|
||||||
|
Checkbox(checked = selectedId.value == i, onCheckedChange = {
|
||||||
|
selectedId.value = if (selectedId.value == i) null else i
|
||||||
|
checked = it
|
||||||
|
if (checked) ytTabsMap[i] = urlEnd else ytTabsMap.remove(i)
|
||||||
|
})
|
||||||
|
Text(text = urlEnd, color = textColor, style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(modifier = Modifier.padding(start = 10.dp, top = 10.dp), onClick = {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
for (i in ytTabsMap.keys) {
|
||||||
|
Logd(TAG, "Subscribing $i ${channelInfo.tabs[i].url}")
|
||||||
|
buildYTChannel(i, ytTabsMap[i]!!) { feed_, map -> handleFeed(feed_, map) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text("Confirm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun buildYTPlaylist(handleFeed: (Feed, Map<String, String>)->Unit) {
|
||||||
try {
|
try {
|
||||||
val service = try { Vista.getService("YouTube") } catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
|
val url = urlInit
|
||||||
|
val playlistInfo = PlaylistInfo.getInfo(service, url) ?: return
|
||||||
selectedDownloadUrl = prepareUrl(url)
|
selectedDownloadUrl = prepareUrl(url)
|
||||||
|
Logd(TAG, "selectedDownloadUrl: $selectedDownloadUrl url: $url")
|
||||||
val feed_ = Feed(selectedDownloadUrl, null)
|
val feed_ = Feed(selectedDownloadUrl, null)
|
||||||
feed_.isBuilding = true
|
feed_.isBuilding = true
|
||||||
feed_.id = Feed.newId()
|
feed_.id = Feed.newId()
|
||||||
|
@ -52,10 +134,6 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
feed_.hasVideoMedia = true
|
feed_.hasVideoMedia = true
|
||||||
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
|
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
|
||||||
val eList: MutableList<Episode> = mutableListOf()
|
val eList: MutableList<Episode> = mutableListOf()
|
||||||
|
|
||||||
val uURL = URL(url)
|
|
||||||
if (uURL.path.startsWith("/playlist") || uURL.path.startsWith("/playlist")) {
|
|
||||||
val playlistInfo = PlaylistInfo.getInfo(Vista.getService(0), url) ?: return@launch
|
|
||||||
feed_.title = playlistInfo.name
|
feed_.title = playlistInfo.name
|
||||||
feed_.description = playlistInfo.description?.content ?: ""
|
feed_.description = playlistInfo.description?.content ?: ""
|
||||||
feed_.author = playlistInfo.uploaderName
|
feed_.author = playlistInfo.uploaderName
|
||||||
|
@ -76,7 +154,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
eList.add(e)
|
eList.add(e)
|
||||||
}
|
}
|
||||||
feed_.episodes.addAll(eList)
|
feed_.episodes.addAll(eList)
|
||||||
if (nextPage == null || feed_.episodes.size > 1000) break
|
if (nextPage == null || feed_.episodes.size > 2000) break
|
||||||
try {
|
try {
|
||||||
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
|
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
|
||||||
nextPage = page.nextPage
|
nextPage = page.nextPage
|
||||||
|
@ -91,17 +169,29 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
feed_.isBuilding = false
|
feed_.isBuilding = false
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
|
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
|
||||||
} else {
|
} catch (e: Throwable) {
|
||||||
val channelInfo = ChannelInfo.getInfo(service, url)
|
Logd(TAG, "startFeedBuilding error ${e.message}")
|
||||||
Logd(TAG, "startFeedBuilding result: $channelInfo ${channelInfo.tabs.size}")
|
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||||
if (channelInfo.tabs.isEmpty()) {
|
|
||||||
withContext(Dispatchers.Main) { showError("Channel is empty", "") }
|
|
||||||
return@launch
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun buildYTChannel(index: Int, title: String, handleFeed: (Feed, Map<String, String>)->Unit) {
|
||||||
|
Logd(TAG, "startFeedBuilding result: $channelInfo ${channelInfo.tabs.size}")
|
||||||
|
var url = channelInfo.tabs[index].url
|
||||||
|
if (!url.startsWith("http")) url = urlInit
|
||||||
try {
|
try {
|
||||||
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first())
|
selectedDownloadUrl = prepareUrl(url)
|
||||||
|
Logd(TAG, "selectedDownloadUrl: $selectedDownloadUrl url: $url")
|
||||||
|
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs[index])
|
||||||
Logd(TAG, "startFeedBuilding result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
|
Logd(TAG, "startFeedBuilding result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
|
||||||
feed_.title = channelInfo.name
|
val feed_ = Feed(selectedDownloadUrl, null)
|
||||||
|
feed_.isBuilding = true
|
||||||
|
feed_.id = Feed.newId()
|
||||||
|
feed_.type = Feed.FeedType.YOUTUBE.name
|
||||||
|
feed_.hasVideoMedia = true
|
||||||
|
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
|
||||||
|
val eList: MutableList<Episode> = mutableListOf()
|
||||||
|
feed_.title = channelInfo.name + " " + title
|
||||||
feed_.description = channelInfo.description
|
feed_.description = channelInfo.description
|
||||||
feed_.author = channelInfo.parentChannelName
|
feed_.author = channelInfo.parentChannelName
|
||||||
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||||
|
@ -122,7 +212,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
eList.add(e)
|
eList.add(e)
|
||||||
}
|
}
|
||||||
feed_.episodes.addAll(eList)
|
feed_.episodes.addAll(eList)
|
||||||
if (nextPage == null || feed_.episodes.size > 1000) break
|
if (nextPage == null || feed_.episodes.size > 2000) break
|
||||||
try {
|
try {
|
||||||
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
|
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
|
||||||
nextPage = page.nextPage
|
nextPage = page.nextPage
|
||||||
|
@ -142,15 +232,8 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logd(TAG, "startFeedBuilding error ${e.message}")
|
|
||||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle normal podcast source
|
fun buildPodcast(url: String, username: String?, password: String?, handleFeed: (Feed, Map<String, String>)->Unit) {
|
||||||
when (val urlType = htmlOrXml(url)) {
|
when (val urlType = htmlOrXml(url)) {
|
||||||
"HTML" -> {
|
"HTML" -> {
|
||||||
val doc = Jsoup.connect(url).get()
|
val doc = Jsoup.connect(url).get()
|
||||||
|
@ -159,7 +242,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
for (element in linkElements) {
|
for (element in linkElements) {
|
||||||
val rssUrl = element.attr("href")
|
val rssUrl = element.attr("href")
|
||||||
Logd(TAG, "RSS URL: $rssUrl")
|
Logd(TAG, "RSS URL: $rssUrl")
|
||||||
startFeedBuilding(rssUrl, username, password) {feed, map -> handleFeed(feed, map) }
|
buildPodcast(rssUrl, username, password) {feed, map -> handleFeed(feed, map) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"XML" -> {}
|
"XML" -> {}
|
||||||
|
@ -280,5 +363,4 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
// }
|
// }
|
||||||
Logd(TAG, "fo.id: ${fo?.id} feed.id: ${feed.id}")
|
Logd(TAG, "fo.id: ${fo?.id} feed.id: ${feed.id}")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -31,6 +31,7 @@ import ac.mdiq.vista.extractor.Vista
|
||||||
import ac.mdiq.vista.extractor.channel.ChannelInfo
|
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.stream.StreamInfoItem
|
import ac.mdiq.vista.extractor.stream.StreamInfoItem
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
|
@ -52,6 +53,7 @@ import io.realm.kotlin.types.RealmList
|
||||||
import org.xml.sax.SAXException
|
import org.xml.sax.SAXException
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.net.URL
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -233,19 +235,23 @@ object FeedUpdateManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun refreshYoutubeFeed(feed: Feed) {
|
private fun refreshYoutubeFeed(feed: Feed) {
|
||||||
|
if (feed.downloadUrl.isNullOrEmpty()) return
|
||||||
|
val url = feed.downloadUrl
|
||||||
try {
|
try {
|
||||||
val service = try { Vista.getService("YouTube") } catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
|
val service = try { Vista.getService("YouTube")
|
||||||
val channelInfo = ChannelInfo.getInfo(service, feed.downloadUrl!!)
|
} catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
|
||||||
|
|
||||||
|
val uURL = URL(url)
|
||||||
|
if (uURL.path.startsWith("/channel")) {
|
||||||
|
val channelInfo = ChannelInfo.getInfo(service, url!!)
|
||||||
Logd(TAG, "refreshYoutubeFeed channelInfo: $channelInfo ${channelInfo.tabs.size}")
|
Logd(TAG, "refreshYoutubeFeed channelInfo: $channelInfo ${channelInfo.tabs.size}")
|
||||||
if (channelInfo.tabs.isEmpty()) return
|
if (channelInfo.tabs.isEmpty()) return
|
||||||
try {
|
try {
|
||||||
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first())
|
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first())
|
||||||
Logd(TAG, "refreshYoutubeFeed result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
|
Logd(TAG, "refreshYoutubeFeed result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
|
||||||
val eList: RealmList<Episode> = realmListOf()
|
val eList: RealmList<Episode> = realmListOf()
|
||||||
for (r in channelTabInfo.relatedItems) {
|
for (r in channelTabInfo.relatedItems) eList.add(episodeFromStreamInfoItem(r as StreamInfoItem))
|
||||||
eList.add(episodeFromStreamInfoItem(r as StreamInfoItem))
|
val feed_ = Feed(url, null)
|
||||||
}
|
|
||||||
val feed_ = Feed(feed.downloadUrl, null)
|
|
||||||
feed_.type = Feed.FeedType.YOUTUBE.name
|
feed_.type = Feed.FeedType.YOUTUBE.name
|
||||||
feed_.hasVideoMedia = true
|
feed_.hasVideoMedia = true
|
||||||
feed_.title = channelInfo.name
|
feed_.title = channelInfo.name
|
||||||
|
@ -255,7 +261,24 @@ object FeedUpdateManager {
|
||||||
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||||
feed_.episodes = eList
|
feed_.episodes = eList
|
||||||
Feeds.updateFeed(applicationContext, feed_, false)
|
Feeds.updateFeed(applicationContext, feed_, false)
|
||||||
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed error1 ${e.message}") }
|
} 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 eList: RealmList<Episode> = realmListOf()
|
||||||
|
try {
|
||||||
|
for (r in playlistInfo.relatedItems) eList.add(episodeFromStreamInfoItem(r))
|
||||||
|
val feed_ = Feed(url, null)
|
||||||
|
feed_.type = Feed.FeedType.YOUTUBE.name
|
||||||
|
feed_.hasVideoMedia = true
|
||||||
|
feed_.title = playlistInfo.name
|
||||||
|
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
|
||||||
|
feed_.description = playlistInfo.description?.content ?: ""
|
||||||
|
feed_.author = playlistInfo.uploaderName
|
||||||
|
feed_.imageUrl = if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
|
||||||
|
feed_.episodes = eList
|
||||||
|
Feeds.updateFeed(applicationContext, feed_, false)
|
||||||
|
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed playlist error1 ${e.message}") }
|
||||||
|
}
|
||||||
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed error ${e.message}") }
|
} catch (e: Throwable) { Logd(TAG, "refreshYoutubeFeed error ${e.message}") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,6 @@ import ac.mdiq.podcini.util.FlowEvent
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.AppOpsManager
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
|
@ -60,8 +59,6 @@ import android.widget.EditText
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
@ -69,7 +66,6 @@ import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
import androidx.media3.session.SessionToken
|
import androidx.media3.session.SessionToken
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
|
@ -986,9 +986,7 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDi
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) { onDismissRequest() }
|
withContext(Dispatchers.Main) { onDismissRequest() }
|
||||||
}
|
}
|
||||||
}) {
|
}) { Text("Confirm") }
|
||||||
Text("Confirm")
|
|
||||||
}
|
|
||||||
} else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp))
|
} else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package ac.mdiq.podcini.ui.compose
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.net.feed.FeedBuilder
|
import ac.mdiq.podcini.net.feed.FeedBuilder
|
||||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
||||||
import ac.mdiq.podcini.storage.database.Feeds
|
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync
|
import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||||
|
@ -55,10 +54,7 @@ fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
for (rating in Rating.entries.reversed()) {
|
for (rating in Rating.entries.reversed()) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
|
||||||
for (item in selected) {
|
for (item in selected) upsertBlk(item) { it.rating = rating.code }
|
||||||
// Feeds.setRating(item, rating.code)
|
|
||||||
upsertBlk(item) { it.rating = rating.code }
|
|
||||||
}
|
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
}) {
|
}) {
|
||||||
Icon(imageVector = ImageVector.vectorResource(id = rating.res), "")
|
Icon(imageVector = ImageVector.vectorResource(id = rating.res), "")
|
||||||
|
@ -85,8 +81,7 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Text(message)
|
Text(message)
|
||||||
Text(stringResource(R.string.feed_delete_reason_msg))
|
Text(stringResource(R.string.feed_delete_reason_msg))
|
||||||
BasicTextField(value = textState, onValueChange = { textState = it },
|
BasicTextField(value = textState, onValueChange = { textState = it }, textStyle = TextStyle(fontSize = 16.sp, color = textColor),
|
||||||
textStyle = TextStyle(fontSize = 16.sp, color = textColor),
|
|
||||||
modifier = Modifier.fillMaxWidth().height(100.dp).padding(start = 10.dp, end = 10.dp, bottom = 10.dp).border(1.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small)
|
modifier = Modifier.fillMaxWidth().height(100.dp).padding(start = 10.dp, end = 10.dp, bottom = 10.dp).border(1.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small)
|
||||||
)
|
)
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
|
@ -119,9 +114,7 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
|
||||||
} catch (e: Throwable) { Log.e("RemoveFeedDialog", Log.getStackTraceString(e)) }
|
} catch (e: Throwable) { Log.e("RemoveFeedDialog", Log.getStackTraceString(e)) }
|
||||||
}
|
}
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
}) {
|
}) { Text("Confirm") }
|
||||||
Text("Confirm")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,41 +123,43 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: SubscriptionLog? = null) {
|
fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: SubscriptionLog? = null) {
|
||||||
|
// var showYTChannelDialog by remember { mutableStateOf(false) }
|
||||||
|
// if (showYTChannelDialog) feedBuilder.ConfirmYTChannelTabsDialog(onDismissRequest = {showYTChannelDialog = false}) {feed, map -> handleFeed(feed, map)}
|
||||||
|
|
||||||
val showSubscribeDialog = remember { mutableStateOf(false) }
|
val showSubscribeDialog = remember { mutableStateOf(false) }
|
||||||
@Composable
|
@Composable
|
||||||
fun confirmSubscribe(feed: PodcastSearchResult, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
fun confirmSubscribe(feed: PodcastSearchResult, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||||
if (showDialog) {
|
if (showDialog) {
|
||||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp),
|
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) {
|
||||||
shape = RoundedCornerShape(16.dp)) {
|
val textColor = MaterialTheme.colorScheme.onSurface
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||||
Text("Subscribe: \"${feed.title}\" ?")
|
Text("Subscribe: \"${feed.title}\" ?", color = textColor, modifier = Modifier.padding(bottom = 10.dp))
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
if (feed.feedUrl != null) {
|
if (feed.feedUrl != null) {
|
||||||
val feedBuilder = FeedBuilder(activity) { message, details ->
|
val feedBuilder = FeedBuilder(activity) { message, details -> Logd("OnineFeedItem", "Subscribe error: $message \n $details") }
|
||||||
Logd("OnineFeedItem", "Subscribe error: $message \n $details")
|
|
||||||
}
|
|
||||||
feedBuilder.feedSource = feed.source
|
feedBuilder.feedSource = feed.source
|
||||||
feedBuilder.startFeedBuilding(feed.feedUrl,
|
val url = feed.feedUrl
|
||||||
"",
|
if (feedBuilder.isYoutube(url)) {
|
||||||
"") { feed, _ -> feedBuilder.subscribe(feed) }
|
if (feedBuilder.isYoutubeChannel()) {
|
||||||
|
val nTabs = feedBuilder.youtubeChannelValidTabs()
|
||||||
|
feedBuilder.buildYTChannel(0, "") { feed, _ -> feedBuilder.subscribe(feed) }
|
||||||
|
// if (nTabs > 1) showYTChannelDialog = true
|
||||||
|
// else feedBuilder.buildYTChannel(0, "") { feed, map -> feedBuilder.subscribe(feed) }
|
||||||
|
} else feedBuilder.buildYTPlaylist { feed, _ -> feedBuilder.subscribe(feed) }
|
||||||
|
} else feedBuilder.buildPodcast(url, "", "") { feed, _ -> feedBuilder.subscribe(feed) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
}) {
|
}) { Text("Confirm") }
|
||||||
Text("Confirm")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (showSubscribeDialog.value) confirmSubscribe(feed, showSubscribeDialog.value, onDismissRequest = { showSubscribeDialog.value = false })
|
||||||
if (showSubscribeDialog.value) {
|
|
||||||
confirmSubscribe(feed, showSubscribeDialog.value, onDismissRequest = {
|
|
||||||
showSubscribeDialog.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Column(Modifier.padding(start = 10.dp, end = 10.dp, top = 4.dp, bottom = 4.dp).combinedClickable(
|
Column(Modifier.padding(start = 10.dp, end = 10.dp, top = 4.dp, bottom = 4.dp).combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
@ -180,11 +175,7 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
|
||||||
}
|
}
|
||||||
}, onLongClick = { showSubscribeDialog.value = true })) {
|
}, onLongClick = { showSubscribeDialog.value = true })) {
|
||||||
val textColor = MaterialTheme.colorScheme.onSurface
|
val textColor = MaterialTheme.colorScheme.onSurface
|
||||||
Text(feed.title,
|
Text(feed.title, color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(bottom = 4.dp))
|
||||||
color = textColor,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.padding(bottom = 4.dp))
|
|
||||||
Row {
|
Row {
|
||||||
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
|
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
|
||||||
val (imgvCover, checkMark) = createRefs()
|
val (imgvCover, checkMark) = createRefs()
|
||||||
|
@ -196,7 +187,6 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
|
||||||
bottom.linkTo(parent.bottom)
|
bottom.linkTo(parent.bottom)
|
||||||
start.linkTo(parent.start)
|
start.linkTo(parent.start)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (feed.feedId > 0 || log != null) {
|
if (feed.feedId > 0 || log != null) {
|
||||||
Logd("OnlineFeedItem", "${feed.feedId} $log")
|
Logd("OnlineFeedItem", "${feed.feedId} $log")
|
||||||
val alpha = 1.0f
|
val alpha = 1.0f
|
||||||
|
@ -215,26 +205,14 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
|
||||||
feed.feedUrl != null && !feed.feedUrl.contains("itunes.apple.com") -> feed.feedUrl
|
feed.feedUrl != null && !feed.feedUrl.contains("itunes.apple.com") -> feed.feedUrl
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
if (authorText.isNotEmpty()) Text(authorText,
|
if (authorText.isNotEmpty()) Text(authorText, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||||
color = textColor,
|
if (feed.subscriberCount > 0) Text(MiscFormatter.formatNumber(feed.subscriberCount) + " subscribers", color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||||
style = MaterialTheme.typography.bodyMedium)
|
|
||||||
if (feed.subscriberCount > 0) Text(MiscFormatter.formatNumber(feed.subscriberCount) + " subscribers",
|
|
||||||
color = textColor,
|
|
||||||
style = MaterialTheme.typography.bodyMedium)
|
|
||||||
Row {
|
Row {
|
||||||
if (feed.count != null && feed.count > 0) Text(feed.count.toString() + " episodes",
|
if (feed.count != null && feed.count > 0) Text(feed.count.toString() + " episodes", color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||||
color = textColor,
|
|
||||||
style = MaterialTheme.typography.bodyMedium)
|
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
if (feed.update != null) Text(feed.update,
|
if (feed.update != null) Text(feed.update, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||||
color = textColor,
|
|
||||||
style = MaterialTheme.typography.bodyMedium)
|
|
||||||
}
|
}
|
||||||
Text(feed.source + ": " + feed.feedUrl,
|
Text(feed.source + ": " + feed.feedUrl, color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelSmall)
|
||||||
color = textColor,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.labelSmall)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,6 @@ import kotlin.concurrent.Volatile
|
||||||
* If the feed cannot be downloaded or parsed, an error dialog will be displayed
|
* If the feed cannot be downloaded or parsed, an error dialog will be displayed
|
||||||
* and the activity will finish as soon as the error dialog is closed.
|
* and the activity will finish as soon as the error dialog is closed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class OnlineFeedFragment : Fragment() {
|
class OnlineFeedFragment : Fragment() {
|
||||||
private var _binding: ComposeFragmentBinding? = null
|
private var _binding: ComposeFragmentBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
@ -86,7 +85,9 @@ class OnlineFeedFragment : Fragment() {
|
||||||
|
|
||||||
var feedSource: String = ""
|
var feedSource: String = ""
|
||||||
private var feedUrl: String = ""
|
private var feedUrl: String = ""
|
||||||
|
private var urlToLog: String = ""
|
||||||
private lateinit var feedBuilder: FeedBuilder
|
private lateinit var feedBuilder: FeedBuilder
|
||||||
|
private var showYTChannelDialog by mutableStateOf(false)
|
||||||
|
|
||||||
private var isShared: Boolean = false
|
private var isShared: Boolean = false
|
||||||
|
|
||||||
|
@ -134,6 +135,7 @@ class OnlineFeedFragment : Fragment() {
|
||||||
|
|
||||||
binding.mainView.setContent {
|
binding.mainView.setContent {
|
||||||
CustomTheme(requireContext()) {
|
CustomTheme(requireContext()) {
|
||||||
|
if (showYTChannelDialog) feedBuilder.ConfirmYTChannelTabsDialog(onDismissRequest = {showYTChannelDialog = false}) {feed, map -> handleFeed(feed, map)}
|
||||||
MainView()
|
MainView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,16 +183,11 @@ class OnlineFeedFragment : Fragment() {
|
||||||
outState.putString("password", password)
|
outState.putString("password", password)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun lookupUrlAndBuild(url: String) {
|
private fun handleFeed(feed_: Feed, map: Map<String, String>) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val urlString = PodcastSearcherRegistry.lookupUrl1(url)
|
|
||||||
try {
|
|
||||||
feeds = getFeedList()
|
|
||||||
feedBuilder.startFeedBuilding(urlString, username, password) { feed_, map ->
|
|
||||||
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
||||||
feed = feed_
|
feed = feed_
|
||||||
if (isShared) {
|
if (isShared) {
|
||||||
val log = realm.query(ShareLog::class).query("url == $0", url).first().find()
|
val log = realm.query(ShareLog::class).query("url == $0", urlToLog).first().find()
|
||||||
if (log != null) upsertBlk(log) {
|
if (log != null) upsertBlk(log) {
|
||||||
it.title = feed_.title
|
it.title = feed_.title
|
||||||
it.author = feed_.author
|
it.author = feed_.author
|
||||||
|
@ -198,6 +195,20 @@ class OnlineFeedFragment : Fragment() {
|
||||||
}
|
}
|
||||||
showFeedInformation(feed_, map)
|
showFeedInformation(feed_, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun lookupUrlAndBuild(url: String) {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
urlToLog = url
|
||||||
|
val urlString = PodcastSearcherRegistry.lookupUrl1(url)
|
||||||
|
try {
|
||||||
|
feeds = getFeedList()
|
||||||
|
if (feedBuilder.isYoutube(urlString)) {
|
||||||
|
if (feedBuilder.isYoutubeChannel()) {
|
||||||
|
val nTabs = feedBuilder.youtubeChannelValidTabs()
|
||||||
|
if (nTabs > 1) showYTChannelDialog = true
|
||||||
|
else feedBuilder.buildYTChannel(0, "") { feed_, map -> handleFeed(feed_, map) }
|
||||||
|
} else feedBuilder.buildYTPlaylist { feed_, map -> handleFeed(feed_, map) }
|
||||||
|
} else feedBuilder.buildPodcast(urlString, username, password) { feed_, map -> handleFeed(feed_, map) }
|
||||||
} catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e)
|
} catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
@ -223,21 +234,17 @@ class OnlineFeedFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
|
urlToLog = url
|
||||||
Logd(TAG, "Successfully retrieve feed url")
|
Logd(TAG, "Successfully retrieve feed url")
|
||||||
isFeedFoundBySearch = true
|
isFeedFoundBySearch = true
|
||||||
feeds = getFeedList()
|
feeds = getFeedList()
|
||||||
feedBuilder.startFeedBuilding(url, username, password) { feed_, map ->
|
if (feedBuilder.isYoutube(url)) {
|
||||||
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
if (feedBuilder.isYoutubeChannel()) {
|
||||||
feed = feed_
|
val nTabs = feedBuilder.youtubeChannelValidTabs()
|
||||||
if (isShared) {
|
if (nTabs > 1) showYTChannelDialog = true
|
||||||
val log = realm.query(ShareLog::class).query("url == $0", url).first().find()
|
else feedBuilder.buildYTChannel(0, "") { feed_, map -> handleFeed(feed_, map) }
|
||||||
if (log != null) upsertBlk(log) {
|
} else feedBuilder.buildYTPlaylist { feed_, map -> handleFeed(feed_, map) }
|
||||||
it.title = feed_.title
|
} else feedBuilder.buildPodcast(url, username, password) { feed_, map -> handleFeed(feed_, map) }
|
||||||
it.author = feed_.author
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showFeedInformation(feed_, map)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
showNoPodcastFoundError()
|
showNoPodcastFoundError()
|
||||||
Logd(TAG, "Failed to retrieve feed url")
|
Logd(TAG, "Failed to retrieve feed url")
|
||||||
|
@ -352,20 +359,19 @@ class OnlineFeedFragment : Fragment() {
|
||||||
val (progressBar, main) = createRefs()
|
val (progressBar, main) = createRefs()
|
||||||
if (showProgress) CircularProgressIndicator(progress = { 0.6f },
|
if (showProgress) CircularProgressIndicator(progress = { 0.6f },
|
||||||
strokeWidth = 10.dp, modifier = Modifier.size(50.dp).constrainAs(progressBar) { centerTo(parent) })
|
strokeWidth = 10.dp, modifier = Modifier.size(50.dp).constrainAs(progressBar) { centerTo(parent) })
|
||||||
else Column(modifier = Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp)
|
else Column(modifier = Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp).constrainAs(main) {
|
||||||
.constrainAs(main) {
|
|
||||||
top.linkTo(parent.top)
|
top.linkTo(parent.top)
|
||||||
bottom.linkTo(parent.bottom)
|
bottom.linkTo(parent.bottom)
|
||||||
start.linkTo(parent.start)
|
start.linkTo(parent.start)
|
||||||
}) {
|
}) {
|
||||||
if (showFeedDisplay) ConstraintLayout(modifier = Modifier.fillMaxWidth().height(120.dp).background(MaterialTheme.colorScheme.surface)) {
|
if (showFeedDisplay) ConstraintLayout(modifier = Modifier.fillMaxWidth().height(120.dp).background(MaterialTheme.colorScheme.surface)) {
|
||||||
val (backgroundImage, coverImage, taColumn, buttons, closeButton) = createRefs()
|
val (backgroundImage, coverImage, taColumn, buttons, closeButton) = createRefs()
|
||||||
if (false) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_settings_white), contentDescription = "background",
|
// if (false) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_settings_white), contentDescription = "background",
|
||||||
Modifier.fillMaxWidth().height(120.dp).constrainAs(backgroundImage) {
|
// Modifier.fillMaxWidth().height(120.dp).constrainAs(backgroundImage) {
|
||||||
top.linkTo(parent.top)
|
// top.linkTo(parent.top)
|
||||||
bottom.linkTo(parent.bottom)
|
// bottom.linkTo(parent.bottom)
|
||||||
start.linkTo(parent.start)
|
// start.linkTo(parent.start)
|
||||||
})
|
// })
|
||||||
AsyncImage(model = feed?.imageUrl?:"", contentDescription = "coverImage", error = painterResource(R.mipmap.ic_launcher),
|
AsyncImage(model = feed?.imageUrl?:"", contentDescription = "coverImage", error = painterResource(R.mipmap.ic_launcher),
|
||||||
modifier = Modifier.width(100.dp).height(100.dp).padding(start = 10.dp, end = 16.dp, bottom = 10.dp).constrainAs(coverImage) {
|
modifier = Modifier.width(100.dp).height(100.dp).padding(start = 10.dp, end = 16.dp, bottom = 10.dp).constrainAs(coverImage) {
|
||||||
bottom.linkTo(parent.bottom)
|
bottom.linkTo(parent.bottom)
|
||||||
|
@ -387,9 +393,7 @@ class OnlineFeedFragment : Fragment() {
|
||||||
if (feedInFeedlist() || isSubscribed(feed!!)) {
|
if (feedInFeedlist() || isSubscribed(feed!!)) {
|
||||||
if (isShared) {
|
if (isShared) {
|
||||||
val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find()
|
val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find()
|
||||||
if (log != null) upsertBlk(log) {
|
if (log != null) upsertBlk(log) { it.status = ShareLog.Status.EXISTING.ordinal }
|
||||||
it.status = ShareLog.Status.EXISTING.ordinal
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val feed = getFeedByTitleAndAuthor(feed?.eigenTitle?:"", feed?.author?:"")
|
val feed = getFeedByTitleAndAuthor(feed?.eigenTitle?:"", feed?.author?:"")
|
||||||
if (feed != null ) (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed))
|
if (feed != null ) (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed))
|
||||||
|
@ -402,9 +406,7 @@ class OnlineFeedFragment : Fragment() {
|
||||||
feedBuilder.subscribe(feed!!)
|
feedBuilder.subscribe(feed!!)
|
||||||
if (isShared) {
|
if (isShared) {
|
||||||
val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find()
|
val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find()
|
||||||
if (log != null) upsertBlk(log) {
|
if (log != null) upsertBlk(log) { it.status = ShareLog.Status.SUCCESS.ordinal }
|
||||||
it.status = ShareLog.Status.SUCCESS.ordinal
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
enableSubscribe = true
|
enableSubscribe = true
|
||||||
|
@ -413,20 +415,16 @@ class OnlineFeedFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
}) { Text(stringResource(subButTextRes)) }
|
||||||
Text(stringResource(subButTextRes))
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.weight(0.1f))
|
Spacer(modifier = Modifier.weight(0.1f))
|
||||||
if (enableEpisodes && feed != null) Button(onClick = { showEpisodes(feed!!.episodes) }) {
|
if (enableEpisodes && feed != null) Button(onClick = { showEpisodes(feed!!.episodes) }) { Text(stringResource(R.string.episodes_label)) }
|
||||||
Text(stringResource(R.string.episodes_label))
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.weight(0.2f))
|
Spacer(modifier = Modifier.weight(0.2f))
|
||||||
}
|
}
|
||||||
if (false) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_close_white), contentDescription = null, modifier = Modifier
|
// if (false) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_close_white), contentDescription = null, modifier = Modifier
|
||||||
.constrainAs(closeButton) {
|
// .constrainAs(closeButton) {
|
||||||
top.linkTo(parent.top)
|
// top.linkTo(parent.top)
|
||||||
end.linkTo(parent.end)
|
// end.linkTo(parent.end)
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
// alternate_urls_spinner
|
// alternate_urls_spinner
|
||||||
|
@ -706,7 +704,6 @@ class OnlineFeedFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RemoteEpisodesFragment : BaseEpisodesFragment() {
|
class RemoteEpisodesFragment : BaseEpisodesFragment() {
|
||||||
private val episodeList: MutableList<Episode> = mutableListOf()
|
private val episodeList: MutableList<Episode> = mutableListOf()
|
||||||
|
|
||||||
|
|
|
@ -148,7 +148,8 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
|
|
||||||
spinnerView = ComposeView(requireContext()).apply {
|
spinnerView = ComposeView(requireContext()).apply {
|
||||||
setContent {
|
setContent {
|
||||||
Spinner(items = spinnerTexts, selectedItem = spinnerTexts[0]) { index:Int ->
|
CustomTheme(requireContext()) {
|
||||||
|
Spinner(items = spinnerTexts, selectedItem = spinnerTexts[0]) { index: Int ->
|
||||||
Logd(TAG, "Queue selected: $queues[index].name")
|
Logd(TAG, "Queue selected: $queues[index].name")
|
||||||
val prevQueueSize = curQueue.size()
|
val prevQueueSize = curQueue.size()
|
||||||
curQueue = upsertBlk(queues[index]) { it.update() }
|
curQueue = upsertBlk(queues[index]) { it.update() }
|
||||||
|
@ -158,6 +159,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
toolbar.addView(spinnerView)
|
toolbar.addView(spinnerView)
|
||||||
|
|
||||||
swipeActions = SwipeActions(this, TAG)
|
swipeActions = SwipeActions(this, TAG)
|
||||||
|
|
|
@ -65,7 +65,6 @@ import java.text.NumberFormat
|
||||||
/**
|
/**
|
||||||
* Performs a search operation on all feeds or one specific feed and displays the search result.
|
* Performs a search operation on all feeds or one specific feed and displays the search result.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class SearchFragment : Fragment() {
|
class SearchFragment : Fragment() {
|
||||||
private var _binding: SearchFragmentBinding? = null
|
private var _binding: SearchFragmentBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
@ -120,7 +119,6 @@ class SearchFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshSwipeTelltale()
|
refreshSwipeTelltale()
|
||||||
|
|
||||||
chip = binding.feedTitleChip
|
chip = binding.feedTitleChip
|
||||||
chip.setOnCloseIconClickListener {
|
chip.setOnCloseIconClickListener {
|
||||||
requireArguments().putLong(ARG_FEED, 0)
|
requireArguments().putLong(ARG_FEED, 0)
|
||||||
|
|
|
@ -54,7 +54,8 @@ class FeedStatisticsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showStats(s: StatisticsItem?) {
|
private fun showStats(s: StatisticsItem?) {
|
||||||
binding.startedTotalLabel.text = String.format(Locale.getDefault(), "%d / %d", s!!.episodesStarted, s.numEpisodes)
|
if (s == null) return
|
||||||
|
binding.startedTotalLabel.text = String.format(Locale.getDefault(), "%d / %d", s.episodesStarted, s.numEpisodes)
|
||||||
binding.timePlayedLabel.text = shortLocalizedDuration(requireContext(), s.timePlayed)
|
binding.timePlayedLabel.text = shortLocalizedDuration(requireContext(), s.timePlayed)
|
||||||
binding.totalDurationLabel.text = shortLocalizedDuration(requireContext(), s.time)
|
binding.totalDurationLabel.text = shortLocalizedDuration(requireContext(), s.time)
|
||||||
binding.onDeviceLabel.text = String.format(Locale.getDefault(), "%d", s.episodesDownloadCount)
|
binding.onDeviceLabel.text = String.format(Locale.getDefault(), "%d", s.episodesDownloadCount)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M8,8H6v7c0,1.1 0.9,2 2,2h9v-2H8V8z"/>
|
<path android:fillColor="?attr/action_icon_color" android:pathData="M8,8H6v7c0,1.1 0.9,2 2,2h9v-2H8V8z"/>
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M20,3h-8c-1.1,0 -2,0.9 -2,2v6c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V5C22,3.9 21.1,3 20,3zM20,11h-8V7h8V11z"/>
|
<path android:fillColor="?attr/action_icon_color" android:pathData="M20,3h-8c-1.1,0 -2,0.9 -2,2v6c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V5C22,3.9 21.1,3 20,3zM20,11h-8V7h8V11z"/>
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M4,12H2v7c0,1.1 0.9,2 2,2h9v-2H4V12z"/>
|
<path android:fillColor="?attr/action_icon_color" android:pathData="M4,12H2v7c0,1.1 0.9,2 2,2h9v-2H4V12z"/>
|
||||||
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -276,6 +276,8 @@
|
||||||
<string name="add_opinion_label">Add opinion</string>
|
<string name="add_opinion_label">Add opinion</string>
|
||||||
<string name="cancelled_on_label">Cancelled on</string>
|
<string name="cancelled_on_label">Cancelled on</string>
|
||||||
|
|
||||||
|
<string name="choose_tab">Choose a tab in the channel</string>
|
||||||
|
|
||||||
<string name="set_play_state_label">Set played state</string>
|
<string name="set_play_state_label">Set played state</string>
|
||||||
|
|
||||||
<string name="mark_read_no_media_label">Mark as read</string>
|
<string name="mark_read_no_media_label">Mark as read</string>
|
||||||
|
|
12
changelog.md
12
changelog.md
|
@ -1,3 +1,15 @@
|
||||||
|
# 6.13.5
|
||||||
|
|
||||||
|
* hopefully fixed youtube playlists/podcasts no-updating issue
|
||||||
|
* in OnlineFeed, when subscribing to a Youtube channel, a popup allows to choose a tab (if multiples are available)
|
||||||
|
* tab options include Videos (or customized name, the default tab) and Streams (live tab)
|
||||||
|
* only one tab can be chosen at a time
|
||||||
|
* the tab name will be appended to the feed title
|
||||||
|
* each tab results in a feed/podcast in Podcini
|
||||||
|
* the shortcut way of subscribing (long-click on a search result) only subscribes to the default tab
|
||||||
|
* episodes limits on subscribing to YT channels/playlists/podcasts are raised to 2000
|
||||||
|
* fixed toolbar contrasts on Queues view
|
||||||
|
|
||||||
# 6.13.4
|
# 6.13.4
|
||||||
|
|
||||||
* in Queues view, reworked the spinner in Compose and added associated feeds toggle
|
* in Queues view, reworked the spinner in Compose and added associated feeds toggle
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
Version 6.13.5
|
||||||
|
|
||||||
|
* hopefully fixed youtube playlists/podcasts no-updating issue
|
||||||
|
* in OnlineFeed, when subscribing to a Youtube channel, a popup allows to choose a tab (if multiples are available)
|
||||||
|
* tab options include Videos (or customized name, the default tab) and Streams (live tab)
|
||||||
|
* only one tab can be chosen at a time
|
||||||
|
* the tab name will be appended to the feed title
|
||||||
|
* each tab results in a feed/podcast in Podcini
|
||||||
|
* the shortcut way of subscribing (long-click on a search result) only subscribes to the default tab
|
||||||
|
* episodes limits on subscribing to YT channels/playlists/podcasts are raised to 2000
|
||||||
|
* fixed toolbar contrasts on Queues view
|
Loading…
Reference in New Issue