mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-27 15:30:31 +05:30
feat: add support for using local NewPipe Extractor
This commit is contained in:
parent
1dabd07de4
commit
7db8212c72
@ -0,0 +1,7 @@
|
||||
package com.github.libretube.api
|
||||
|
||||
class LocalStreamsExtractionPipedMediaServiceRepository: PipedMediaServiceRepository() {
|
||||
private val newPipeDelegate = NewPipeMediaServiceRepository()
|
||||
|
||||
override suspend fun getStreams(videoId: String) = newPipeDelegate.getStreams(videoId)
|
||||
}
|
@ -9,16 +9,27 @@ import com.github.libretube.api.obj.SearchResult
|
||||
import com.github.libretube.api.obj.SegmentData
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.helpers.PlayerHelper
|
||||
|
||||
interface MediaServiceRepository {
|
||||
suspend fun getTrending(region: String): List<StreamItem>
|
||||
suspend fun getStreams(videoId: String): Streams
|
||||
suspend fun getComments(videoId: String): CommentsPage
|
||||
suspend fun getSegments(videoId: String, category: String, actionType: String? = null): SegmentData
|
||||
suspend fun getSegments(
|
||||
videoId: String,
|
||||
category: String,
|
||||
actionType: String? = null
|
||||
): SegmentData
|
||||
|
||||
suspend fun getDeArrowContent(videoIds: String): Map<String, DeArrowContent>
|
||||
suspend fun getCommentsNextPage(videoId: String, nextPage: String): CommentsPage
|
||||
suspend fun getSearchResults(searchQuery: String, filter: String): SearchResult
|
||||
suspend fun getSearchResultsNextPage(searchQuery: String, filter: String, nextPage: String): SearchResult
|
||||
suspend fun getSearchResultsNextPage(
|
||||
searchQuery: String,
|
||||
filter: String,
|
||||
nextPage: String
|
||||
): SearchResult
|
||||
|
||||
suspend fun getSuggestions(query: String): List<String>
|
||||
suspend fun getChannel(channelId: String): Channel
|
||||
suspend fun getChannelTab(data: String, nextPage: String? = null): ChannelTabResponse
|
||||
@ -29,7 +40,12 @@ interface MediaServiceRepository {
|
||||
|
||||
companion object {
|
||||
val instance by lazy {
|
||||
PipedMediaServiceRepository()
|
||||
if (PlayerHelper.disablePipedProxy && PlayerHelper.localStreamExtraction) {
|
||||
// TODO: LocalStreamsExtractionPipedMediaServiceRepository()
|
||||
NewPipeMediaServiceRepository()
|
||||
} else {
|
||||
PipedMediaServiceRepository()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,467 @@
|
||||
package com.github.libretube.api
|
||||
|
||||
import com.github.libretube.api.obj.Channel
|
||||
import com.github.libretube.api.obj.ChannelTab
|
||||
import com.github.libretube.api.obj.ChannelTabResponse
|
||||
import com.github.libretube.api.obj.ChapterSegment
|
||||
import com.github.libretube.api.obj.Comment
|
||||
import com.github.libretube.api.obj.CommentsPage
|
||||
import com.github.libretube.api.obj.ContentItem
|
||||
import com.github.libretube.api.obj.DeArrowContent
|
||||
import com.github.libretube.api.obj.MetaInfo
|
||||
import com.github.libretube.api.obj.PipedStream
|
||||
import com.github.libretube.api.obj.Playlist
|
||||
import com.github.libretube.api.obj.PreviewFrames
|
||||
import com.github.libretube.api.obj.SearchResult
|
||||
import com.github.libretube.api.obj.SegmentData
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.api.obj.StreamItem.Companion.TYPE_CHANNEL
|
||||
import com.github.libretube.api.obj.StreamItem.Companion.TYPE_PLAYLIST
|
||||
import com.github.libretube.api.obj.StreamItem.Companion.TYPE_STREAM
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.api.obj.Subtitle
|
||||
import com.github.libretube.extensions.toID
|
||||
import com.github.libretube.helpers.NewPipeExtractorInstance
|
||||
import com.github.libretube.helpers.PlayerHelper
|
||||
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.toKotlinInstant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.schabi.newpipe.extractor.InfoItem
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
|
||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfo
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
||||
import org.schabi.newpipe.extractor.search.SearchInfo
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream
|
||||
|
||||
|
||||
private fun VideoStream.toPipedStream() = PipedStream(
|
||||
url = content,
|
||||
codec = codec,
|
||||
format = format?.toString(),
|
||||
height = height,
|
||||
width = width,
|
||||
quality = getResolution(),
|
||||
mimeType = format?.mimeType,
|
||||
bitrate = bitrate,
|
||||
initStart = initStart,
|
||||
initEnd = initEnd,
|
||||
indexStart = indexStart,
|
||||
indexEnd = indexEnd,
|
||||
fps = fps,
|
||||
contentLength = itagItem?.contentLength ?: 0L
|
||||
)
|
||||
|
||||
private fun AudioStream.toPipedStream() = PipedStream(
|
||||
url = content,
|
||||
format = format?.toString(),
|
||||
quality = "$averageBitrate bits",
|
||||
bitrate = bitrate,
|
||||
mimeType = format?.mimeType,
|
||||
initStart = initStart,
|
||||
initEnd = initEnd,
|
||||
indexStart = indexStart,
|
||||
indexEnd = indexEnd,
|
||||
contentLength = itagItem?.contentLength ?: 0L,
|
||||
codec = codec,
|
||||
audioTrackId = audioTrackId,
|
||||
audioTrackName = audioTrackName,
|
||||
audioTrackLocale = audioLocale?.toLanguageTag(),
|
||||
audioTrackType = audioTrackType?.name,
|
||||
videoOnly = false
|
||||
)
|
||||
|
||||
fun StreamInfoItem.toStreamItem(
|
||||
uploaderAvatarUrl: String? = null
|
||||
) = StreamItem(
|
||||
type = TYPE_STREAM,
|
||||
url = url.toID(),
|
||||
title = name,
|
||||
uploaded = uploadDate?.offsetDateTime()?.toEpochSecond()?.times(1000) ?: -1,
|
||||
uploadedDate = textualUploadDate ?: uploadDate?.offsetDateTime()?.toLocalDateTime()
|
||||
?.toLocalDate()
|
||||
?.toString(),
|
||||
uploaderName = uploaderName,
|
||||
uploaderUrl = uploaderUrl.toID(),
|
||||
uploaderAvatar = uploaderAvatarUrl ?: uploaderAvatars.maxByOrNull { it.height }?.url,
|
||||
thumbnail = thumbnails.maxByOrNull { it.height }?.url,
|
||||
duration = duration,
|
||||
views = viewCount,
|
||||
uploaderVerified = isUploaderVerified,
|
||||
shortDescription = shortDescription,
|
||||
isShort = isShortFormContent
|
||||
)
|
||||
|
||||
fun InfoItem.toContentItem() = when (this) {
|
||||
is StreamInfoItem -> ContentItem(
|
||||
url = url.toID(),
|
||||
type = TYPE_STREAM,
|
||||
thumbnail = thumbnails.maxByOrNull { it.height }?.url.orEmpty(),
|
||||
title = name,
|
||||
uploaderAvatar = uploaderAvatars.maxByOrNull { it.height }?.url.orEmpty(),
|
||||
uploaderUrl = uploaderUrl.toID(),
|
||||
uploaderName = uploaderName,
|
||||
uploaded = uploadDate?.offsetDateTime()?.toInstant()?.toEpochMilli() ?: -1,
|
||||
isShort = isShortFormContent,
|
||||
views = viewCount,
|
||||
shortDescription = shortDescription,
|
||||
verified = isUploaderVerified,
|
||||
duration = duration
|
||||
)
|
||||
|
||||
is ChannelInfoItem -> ContentItem(
|
||||
url = url.toID(),
|
||||
name = name,
|
||||
type = TYPE_CHANNEL,
|
||||
thumbnail = thumbnails.maxByOrNull { it.height }?.url.orEmpty(),
|
||||
subscribers = subscriberCount,
|
||||
videos = streamCount
|
||||
)
|
||||
|
||||
is PlaylistInfoItem -> ContentItem(
|
||||
url = url.toID(),
|
||||
type = TYPE_PLAYLIST,
|
||||
title = name,
|
||||
shortDescription = description.content,
|
||||
thumbnail = thumbnails.maxByOrNull { it.height }?.url.orEmpty(),
|
||||
videos = streamCount,
|
||||
uploaderVerified = isUploaderVerified,
|
||||
uploaderName = uploaderName,
|
||||
uploaderUrl = uploaderUrl?.toID()
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun ChannelInfo.toChannel() = Channel(
|
||||
id = id,
|
||||
name = name,
|
||||
description = description,
|
||||
verified = isVerified,
|
||||
avatarUrl = avatars.maxByOrNull { it.height }?.url,
|
||||
bannerUrl = banners.maxByOrNull { it.height }?.url,
|
||||
tabs = tabs.filterNot { it.contentFilters.contains(ChannelTabs.VIDEOS) }
|
||||
.map { ChannelTab(it.contentFilters.first().lowercase(), it.toTabDataString()) },
|
||||
subscriberCount = subscriberCount
|
||||
)
|
||||
|
||||
fun PlaylistInfo.toPlaylist() = Playlist(
|
||||
name = name,
|
||||
description = description?.content,
|
||||
thumbnailUrl = thumbnails.maxByOrNull { it.height }?.url,
|
||||
uploaderUrl = uploaderUrl.toID(),
|
||||
bannerUrl = banners.maxByOrNull { it.height }?.url,
|
||||
uploader = uploaderName,
|
||||
uploaderAvatar = uploaderAvatars.maxByOrNull { it.height }?.url,
|
||||
videos = streamCount.toInt(),
|
||||
relatedStreams = relatedItems.map { it.toStreamItem() },
|
||||
nextpage = nextPage?.toNextPageString()
|
||||
)
|
||||
|
||||
fun CommentsInfoItem.toComment() = Comment(
|
||||
author = uploaderName,
|
||||
commentId = commentId,
|
||||
commentText = commentText.content,
|
||||
commentedTime = textualUploadDate,
|
||||
commentorUrl = uploaderUrl.toID(),
|
||||
hearted = isHeartedByUploader,
|
||||
creatorReplied = hasCreatorReply(),
|
||||
likeCount = likeCount.toLong(),
|
||||
pinned = isPinned,
|
||||
verified = isUploaderVerified,
|
||||
replyCount = replyCount.toLong(),
|
||||
repliesPage = replies?.toNextPageString(),
|
||||
thumbnail = thumbnails.maxByOrNull { it.height }?.url.orEmpty()
|
||||
)
|
||||
|
||||
// the following classes are necessary because kotlinx can't deserialize
|
||||
// classes from external libraries as they're not annotated
|
||||
@Serializable
|
||||
private data class NextPage(
|
||||
val url: String? = null,
|
||||
val id: String? = null,
|
||||
val ids: List<String>? = null,
|
||||
val cookies: Map<String, String>? = null,
|
||||
val body: String? = null
|
||||
)
|
||||
|
||||
fun Page.toNextPageString() = JsonHelper.json.encodeToString(
|
||||
NextPage(url, id, ids, cookies, body?.toString())
|
||||
)
|
||||
|
||||
fun String.toPage(): Page = with(JsonHelper.json.decodeFromString<NextPage>(this)) {
|
||||
return Page(url, id, ids, cookies, body?.toByteArray())
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class TabData(
|
||||
val originalUrl: String? = null,
|
||||
val url: String? = null,
|
||||
val id: String? = null,
|
||||
val contentFilters: List<String>? = null,
|
||||
val sortFilter: String? = null,
|
||||
)
|
||||
|
||||
fun ListLinkHandler.toTabDataString() = JsonHelper.json.encodeToString(
|
||||
TabData(originalUrl, url, id, contentFilters, sortFilter)
|
||||
)
|
||||
|
||||
fun String.toListLinkHandler() = with(JsonHelper.json.decodeFromString<TabData>(this)) {
|
||||
ListLinkHandler(originalUrl, url, id, contentFilters, sortFilter)
|
||||
}
|
||||
|
||||
class NewPipeMediaServiceRepository : MediaServiceRepository {
|
||||
override suspend fun getTrending(region: String): List<StreamItem> {
|
||||
val kioskList = NewPipeExtractorInstance.extractor.kioskList
|
||||
kioskList.forceContentCountry(ContentCountry(region))
|
||||
|
||||
val extractor = kioskList.defaultKioskExtractor
|
||||
extractor.fetchPage()
|
||||
|
||||
val info = KioskInfo.getInfo(extractor)
|
||||
return info.relatedItems.filterIsInstance<StreamInfoItem>().map { it.toStreamItem() }
|
||||
}
|
||||
|
||||
override suspend fun getStreams(videoId: String): Streams = withContext(Dispatchers.IO) {
|
||||
val respAsync = async {
|
||||
StreamInfo.getInfo("$YOUTUBE_FRONTEND_URL/watch?v=$videoId")
|
||||
}
|
||||
val dislikesAsync = async {
|
||||
if (PlayerHelper.localRYD) runCatching {
|
||||
RetrofitInstance.externalApi.getVotes(videoId).dislikes
|
||||
}.getOrElse { -1 } else -1
|
||||
}
|
||||
val (resp, dislikes) = Pair(respAsync.await(), dislikesAsync.await())
|
||||
|
||||
Streams(
|
||||
title = resp.name,
|
||||
description = resp.description.content,
|
||||
uploader = resp.uploaderName,
|
||||
uploaderAvatar = resp.uploaderAvatars.maxBy { it.height }.url,
|
||||
uploaderUrl = resp.uploaderUrl.toID(),
|
||||
uploaderVerified = resp.isUploaderVerified,
|
||||
uploaderSubscriberCount = resp.uploaderSubscriberCount,
|
||||
category = resp.category,
|
||||
views = resp.viewCount,
|
||||
likes = resp.likeCount,
|
||||
dislikes = dislikes,
|
||||
license = resp.licence,
|
||||
hls = resp.hlsUrl,
|
||||
dash = resp.dashMpdUrl,
|
||||
tags = resp.tags,
|
||||
metaInfo = resp.metaInfo.map {
|
||||
MetaInfo(
|
||||
it.title,
|
||||
it.content.content,
|
||||
it.urls.map { url -> url.toString() },
|
||||
it.urlTexts
|
||||
)
|
||||
},
|
||||
visibility = resp.privacy.name.lowercase(),
|
||||
duration = resp.duration,
|
||||
uploadTimestamp = resp.uploadDate.offsetDateTime().toInstant().toKotlinInstant(),
|
||||
uploaded = resp.uploadDate.offsetDateTime().toEpochSecond() * 1000,
|
||||
thumbnailUrl = resp.thumbnails.maxBy { it.height }.url,
|
||||
relatedStreams = resp.relatedItems
|
||||
.filterIsInstance<StreamInfoItem>()
|
||||
.map { item -> item.toStreamItem() },
|
||||
chapters = resp.streamSegments.map {
|
||||
ChapterSegment(
|
||||
title = it.title,
|
||||
image = it.previewUrl.orEmpty(),
|
||||
start = it.startTimeSeconds.toLong()
|
||||
)
|
||||
},
|
||||
audioStreams = resp.audioStreams.map { it.toPipedStream() },
|
||||
videoStreams = resp.videoOnlyStreams.map { it.toPipedStream().copy(videoOnly = true) } +
|
||||
resp.videoStreams.map { it.toPipedStream().copy(videoOnly = false) },
|
||||
previewFrames = resp.previewFrames.map {
|
||||
PreviewFrames(
|
||||
it.urls,
|
||||
it.frameWidth,
|
||||
it.frameHeight,
|
||||
it.totalCount,
|
||||
it.durationPerFrame.toLong(),
|
||||
it.framesPerPageX,
|
||||
it.framesPerPageY
|
||||
)
|
||||
},
|
||||
subtitles = resp.subtitles.map {
|
||||
Subtitle(
|
||||
it.content,
|
||||
it.format?.mimeType,
|
||||
it.displayLanguageName,
|
||||
it.languageTag,
|
||||
it.isAutoGenerated
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getSegments(
|
||||
videoId: String,
|
||||
category: String,
|
||||
actionType: String?
|
||||
): SegmentData = SegmentData()
|
||||
|
||||
override suspend fun getDeArrowContent(videoIds: String): Map<String, DeArrowContent> =
|
||||
emptyMap()
|
||||
|
||||
override suspend fun getSearchResults(searchQuery: String, filter: String): SearchResult {
|
||||
val queryHandler = NewPipeExtractorInstance.extractor.searchQHFactory.fromQuery(
|
||||
searchQuery,
|
||||
listOf(filter),
|
||||
null
|
||||
)
|
||||
val searchInfo = SearchInfo.getInfo(NewPipeExtractorInstance.extractor, queryHandler)
|
||||
|
||||
return SearchResult(
|
||||
items = searchInfo.relatedItems.mapNotNull { it.toContentItem() },
|
||||
nextpage = searchInfo.nextPage?.toNextPageString()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getSearchResultsNextPage(
|
||||
searchQuery: String,
|
||||
filter: String,
|
||||
nextPage: String
|
||||
): SearchResult {
|
||||
val queryHandler = NewPipeExtractorInstance.extractor.searchQHFactory.fromQuery(
|
||||
searchQuery,
|
||||
listOf(filter),
|
||||
null
|
||||
)
|
||||
val searchInfo = SearchInfo.getMoreItems(
|
||||
NewPipeExtractorInstance.extractor,
|
||||
queryHandler,
|
||||
nextPage.toPage()
|
||||
)
|
||||
return SearchResult(
|
||||
items = searchInfo.items.mapNotNull { it.toContentItem() },
|
||||
nextpage = searchInfo.nextPage?.toNextPageString()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getSuggestions(query: String): List<String> {
|
||||
return NewPipeExtractorInstance.extractor.suggestionExtractor.suggestionList(query)
|
||||
}
|
||||
|
||||
private suspend fun getLatestVideos(channelInfo: ChannelInfo): Pair<List<StreamItem>, String?> {
|
||||
val relatedTab = channelInfo.tabs.find { it.contentFilters.contains(ChannelTabs.VIDEOS) }
|
||||
if (relatedTab != null) {
|
||||
val relatedStreamsResp = getChannelTab(relatedTab.toTabDataString())
|
||||
return relatedStreamsResp.content.map { it.toStreamItem() } to relatedStreamsResp.nextpage
|
||||
}
|
||||
|
||||
return emptyList<StreamItem>() to null
|
||||
}
|
||||
|
||||
override suspend fun getChannel(channelId: String): Channel {
|
||||
val channelUrl = "$YOUTUBE_FRONTEND_URL/channel/${channelId}"
|
||||
val channelInfo = ChannelInfo.getInfo(NewPipeExtractorInstance.extractor, channelUrl)
|
||||
|
||||
val channel = channelInfo.toChannel()
|
||||
|
||||
val relatedVideos = getLatestVideos(channelInfo)
|
||||
channel.relatedStreams = relatedVideos.first
|
||||
channel.nextpage = relatedVideos.second
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
override suspend fun getChannelTab(data: String, nextPage: String?): ChannelTabResponse {
|
||||
val linkListHandler = data.toListLinkHandler()
|
||||
|
||||
val resp = ChannelTabInfo.getInfo(NewPipeExtractorInstance.extractor, linkListHandler)
|
||||
val newNextPage = resp.nextPage?.toNextPageString()
|
||||
|
||||
val items = resp.relatedItems
|
||||
.mapNotNull { it.toContentItem() }
|
||||
return ChannelTabResponse(items, newNextPage)
|
||||
}
|
||||
|
||||
override suspend fun getChannelByName(channelName: String): Channel {
|
||||
val channelUrl = "$YOUTUBE_FRONTEND_URL/c/${channelName}"
|
||||
val channelInfo = ChannelInfo.getInfo(NewPipeExtractorInstance.extractor, channelUrl)
|
||||
|
||||
val channel = channelInfo.toChannel()
|
||||
|
||||
val relatedVideos = getLatestVideos(channelInfo)
|
||||
channel.relatedStreams = relatedVideos.first
|
||||
channel.nextpage = relatedVideos.second
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
override suspend fun getChannelNextPage(channelId: String, nextPage: String): Channel {
|
||||
val url = "${YOUTUBE_FRONTEND_URL}/channel/${channelId}/videos"
|
||||
val listLinkHandler = ListLinkHandler(url, url, channelId, listOf("videos"), "")
|
||||
val tab = getChannelTab(listLinkHandler.toTabDataString(), nextPage)
|
||||
return Channel(
|
||||
relatedStreams = tab.content.map { it.toStreamItem() },
|
||||
nextpage = tab.nextpage
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getPlaylist(playlistId: String): Playlist {
|
||||
val playlistUrl = "${YOUTUBE_FRONTEND_URL}/playlist?list=${playlistId}"
|
||||
val playlistInfo = PlaylistInfo.getInfo(playlistUrl)
|
||||
|
||||
return playlistInfo.toPlaylist()
|
||||
}
|
||||
|
||||
override suspend fun getPlaylistNextPage(playlistId: String, nextPage: String): Playlist {
|
||||
val playlistUrl = "${YOUTUBE_FRONTEND_URL}/playlist?list=${playlistId}"
|
||||
val playlistInfo = PlaylistInfo.getMoreItems(
|
||||
NewPipeExtractorInstance.extractor,
|
||||
playlistUrl,
|
||||
nextPage.toPage()
|
||||
)
|
||||
|
||||
return Playlist(
|
||||
relatedStreams = playlistInfo.items.map { it.toStreamItem() },
|
||||
nextpage = playlistInfo.nextPage?.toNextPageString()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getComments(videoId: String): CommentsPage {
|
||||
val url = "${YOUTUBE_FRONTEND_URL}/watch?v=$videoId"
|
||||
val commentsInfo = CommentsInfo.getInfo(url)
|
||||
|
||||
return CommentsPage(
|
||||
nextpage = commentsInfo.nextPage?.toNextPageString(),
|
||||
disabled = commentsInfo.isCommentsDisabled,
|
||||
commentCount = commentsInfo.commentsCount.toLong(),
|
||||
comments = commentsInfo.relatedItems.map { it.toComment() }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCommentsNextPage(videoId: String, nextPage: String): CommentsPage {
|
||||
val url = "${YOUTUBE_FRONTEND_URL}/watch?v=$videoId"
|
||||
val commentsInfo = CommentsInfo.getMoreItems(
|
||||
NewPipeExtractorInstance.extractor,
|
||||
url,
|
||||
nextPage.toPage()
|
||||
)
|
||||
|
||||
return CommentsPage(
|
||||
nextpage = commentsInfo.nextPage?.toNextPageString(),
|
||||
comments = commentsInfo.items.map { it.toComment() }
|
||||
)
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import com.github.libretube.api.obj.Channel
|
||||
import com.github.libretube.api.obj.ChannelTabResponse
|
||||
import com.github.libretube.api.obj.CommentsPage
|
||||
import com.github.libretube.api.obj.DeArrowContent
|
||||
import com.github.libretube.api.obj.Message
|
||||
import com.github.libretube.api.obj.Playlist
|
||||
import com.github.libretube.api.obj.SearchResult
|
||||
import com.github.libretube.api.obj.SegmentData
|
||||
@ -12,13 +13,23 @@ import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.helpers.PreferenceHelper
|
||||
import retrofit2.HttpException
|
||||
|
||||
class PipedMediaServiceRepository : MediaServiceRepository {
|
||||
open class PipedMediaServiceRepository : MediaServiceRepository {
|
||||
override suspend fun getTrending(region: String): List<StreamItem> =
|
||||
api.getTrending(region)
|
||||
|
||||
override suspend fun getStreams(videoId: String): Streams =
|
||||
api.getStreams(videoId)
|
||||
override suspend fun getStreams(videoId: String): Streams {
|
||||
return try {
|
||||
api.getStreams(videoId)
|
||||
} catch (e: HttpException) {
|
||||
val errorMessage = e.response()?.errorBody()?.string()?.runCatching {
|
||||
JsonHelper.json.decodeFromString<Message>(this).message
|
||||
}?.getOrNull()
|
||||
|
||||
throw Exception(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getComments(videoId: String): CommentsPage =
|
||||
api.getComments(videoId)
|
||||
|
@ -1,176 +0,0 @@
|
||||
package com.github.libretube.api
|
||||
|
||||
import android.content.Context
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.obj.ChapterSegment
|
||||
import com.github.libretube.api.obj.Message
|
||||
import com.github.libretube.api.obj.MetaInfo
|
||||
import com.github.libretube.api.obj.PipedStream
|
||||
import com.github.libretube.api.obj.PreviewFrames
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.api.obj.Subtitle
|
||||
import com.github.libretube.extensions.toID
|
||||
import com.github.libretube.helpers.PlayerHelper
|
||||
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL
|
||||
import com.github.libretube.util.deArrow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.toKotlinInstant
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
fun VideoStream.toPipedStream() = PipedStream(
|
||||
url = content,
|
||||
codec = codec,
|
||||
format = format?.toString(),
|
||||
height = height,
|
||||
width = width,
|
||||
quality = getResolution(),
|
||||
mimeType = format?.mimeType,
|
||||
bitrate = bitrate,
|
||||
initStart = initStart,
|
||||
initEnd = initEnd,
|
||||
indexStart = indexStart,
|
||||
indexEnd = indexEnd,
|
||||
fps = fps,
|
||||
contentLength = itagItem?.contentLength ?: 0L
|
||||
)
|
||||
|
||||
fun AudioStream.toPipedStream() = PipedStream(
|
||||
url = content,
|
||||
format = format?.toString(),
|
||||
quality = "$averageBitrate bits",
|
||||
bitrate = bitrate,
|
||||
mimeType = format?.mimeType,
|
||||
initStart = initStart,
|
||||
initEnd = initEnd,
|
||||
indexStart = indexStart,
|
||||
indexEnd = indexEnd,
|
||||
contentLength = itagItem?.contentLength ?: 0L,
|
||||
codec = codec,
|
||||
audioTrackId = audioTrackId,
|
||||
audioTrackName = audioTrackName,
|
||||
audioTrackLocale = audioLocale?.toLanguageTag(),
|
||||
audioTrackType = audioTrackType?.name,
|
||||
videoOnly = false
|
||||
)
|
||||
|
||||
fun StreamInfoItem.toStreamItem(
|
||||
uploaderAvatarUrl: String? = null
|
||||
) = StreamItem(
|
||||
type = StreamItem.TYPE_STREAM,
|
||||
url = url.toID(),
|
||||
title = name,
|
||||
uploaded = uploadDate?.offsetDateTime()?.toEpochSecond()?.times(1000) ?: -1,
|
||||
uploadedDate = textualUploadDate ?: uploadDate?.offsetDateTime()?.toLocalDateTime()
|
||||
?.toLocalDate()
|
||||
?.toString(),
|
||||
uploaderName = uploaderName,
|
||||
uploaderUrl = uploaderUrl.toID(),
|
||||
uploaderAvatar = uploaderAvatarUrl ?: uploaderAvatars.maxByOrNull { it.height }?.url,
|
||||
thumbnail = thumbnails.maxByOrNull { it.height }?.url,
|
||||
duration = duration,
|
||||
views = viewCount,
|
||||
uploaderVerified = isUploaderVerified,
|
||||
shortDescription = shortDescription,
|
||||
isShort = isShortFormContent
|
||||
)
|
||||
|
||||
object StreamsExtractor {
|
||||
suspend fun extractStreams(videoId: String): Streams = withContext(Dispatchers.IO) {
|
||||
if (!PlayerHelper.disablePipedProxy || !PlayerHelper.localStreamExtraction) {
|
||||
return@withContext MediaServiceRepository.instance.getStreams(videoId).deArrow(videoId)
|
||||
}
|
||||
|
||||
val respAsync = async {
|
||||
StreamInfo.getInfo("$YOUTUBE_FRONTEND_URL/watch?v=$videoId")
|
||||
}
|
||||
val dislikesAsync = async {
|
||||
if (PlayerHelper.localRYD) runCatching {
|
||||
RetrofitInstance.externalApi.getVotes(videoId).dislikes
|
||||
}.getOrElse { -1 } else -1
|
||||
}
|
||||
val (resp, dislikes) = Pair(respAsync.await(), dislikesAsync.await())
|
||||
|
||||
Streams(
|
||||
title = resp.name,
|
||||
description = resp.description.content,
|
||||
uploader = resp.uploaderName,
|
||||
uploaderAvatar = resp.uploaderAvatars.maxBy { it.height }.url,
|
||||
uploaderUrl = resp.uploaderUrl.toID(),
|
||||
uploaderVerified = resp.isUploaderVerified,
|
||||
uploaderSubscriberCount = resp.uploaderSubscriberCount,
|
||||
category = resp.category,
|
||||
views = resp.viewCount,
|
||||
likes = resp.likeCount,
|
||||
dislikes = dislikes,
|
||||
license = resp.licence,
|
||||
hls = resp.hlsUrl,
|
||||
dash = resp.dashMpdUrl,
|
||||
tags = resp.tags,
|
||||
metaInfo = resp.metaInfo.map {
|
||||
MetaInfo(
|
||||
it.title,
|
||||
it.content.content,
|
||||
it.urls.map { url -> url.toString() },
|
||||
it.urlTexts
|
||||
)
|
||||
},
|
||||
visibility = resp.privacy.name.lowercase(),
|
||||
duration = resp.duration,
|
||||
uploadTimestamp = resp.uploadDate.offsetDateTime().toInstant().toKotlinInstant(),
|
||||
uploaded = resp.uploadDate.offsetDateTime().toEpochSecond() * 1000,
|
||||
thumbnailUrl = resp.thumbnails.maxBy { it.height }.url,
|
||||
relatedStreams = resp.relatedItems
|
||||
.filterIsInstance<StreamInfoItem>()
|
||||
.map { item -> item.toStreamItem() },
|
||||
chapters = resp.streamSegments.map {
|
||||
ChapterSegment(
|
||||
title = it.title,
|
||||
image = it.previewUrl.orEmpty(),
|
||||
start = it.startTimeSeconds.toLong()
|
||||
)
|
||||
},
|
||||
audioStreams = resp.audioStreams.map { it.toPipedStream() },
|
||||
videoStreams = resp.videoOnlyStreams.map { it.toPipedStream().copy(videoOnly = true) } +
|
||||
resp.videoStreams.map { it.toPipedStream().copy(videoOnly = false) },
|
||||
previewFrames = resp.previewFrames.map {
|
||||
PreviewFrames(
|
||||
it.urls,
|
||||
it.frameWidth,
|
||||
it.frameHeight,
|
||||
it.totalCount,
|
||||
it.durationPerFrame.toLong(),
|
||||
it.framesPerPageX,
|
||||
it.framesPerPageY
|
||||
)
|
||||
},
|
||||
subtitles = resp.subtitles.map {
|
||||
Subtitle(
|
||||
it.content,
|
||||
it.format?.mimeType,
|
||||
it.displayLanguageName,
|
||||
it.languageTag,
|
||||
it.isAutoGenerated
|
||||
)
|
||||
}
|
||||
).deArrow(videoId)
|
||||
}
|
||||
|
||||
fun getExtractorErrorMessageString(context: Context, exception: Exception): String {
|
||||
return when (exception) {
|
||||
is IOException -> context.getString(R.string.unknown_error)
|
||||
is HttpException -> exception.response()?.errorBody()?.string()?.runCatching {
|
||||
JsonHelper.json.decodeFromString<Message>(this).message
|
||||
}?.getOrNull() ?: context.getString(R.string.server_error)
|
||||
|
||||
else -> exception.localizedMessage.orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ data class Channel(
|
||||
val avatarUrl: String? = null,
|
||||
val bannerUrl: String? = null,
|
||||
val description: String? = null,
|
||||
val nextpage: String? = null,
|
||||
var nextpage: String? = null,
|
||||
val subscriberCount: Long = 0,
|
||||
val verified: Boolean = false,
|
||||
var relatedStreams: List<StreamItem> = emptyList(),
|
||||
|
@ -6,6 +6,4 @@ import kotlinx.serialization.Serializable
|
||||
data class SearchResult(
|
||||
var items: List<ContentItem> = emptyList(),
|
||||
val nextpage: String? = null,
|
||||
val suggestion: String? = null,
|
||||
val corrected: Boolean? = null
|
||||
)
|
||||
|
@ -3,7 +3,6 @@ package com.github.libretube.repo
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.PlaylistsHelper
|
||||
import com.github.libretube.api.PlaylistsHelper.MAX_CONCURRENT_IMPORT_CALLS
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.obj.Playlist
|
||||
import com.github.libretube.api.obj.Playlists
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
@ -125,7 +124,7 @@ class LocalPlaylistsRepository: PlaylistRepository {
|
||||
// Only do so with `MAX_CONCURRENT_IMPORT_CALLS` videos at once to prevent performance issues
|
||||
for (videoIdList in playlist.videos.chunked(MAX_CONCURRENT_IMPORT_CALLS)) {
|
||||
val streams = videoIdList.parallelMap {
|
||||
runCatching { StreamsExtractor.extractStreams(it) }
|
||||
runCatching { MediaServiceRepository.instance.getStreams(it) }
|
||||
.getOrNull()
|
||||
?.toStreamItem(it)
|
||||
}.filterNotNull()
|
||||
|
@ -20,7 +20,7 @@ import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.LibreTubeApp.Companion.DOWNLOAD_CHANNEL_NAME
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.db.DatabaseHolder.Database
|
||||
@ -124,12 +124,13 @@ class DownloadService : LifecycleService() {
|
||||
lifecycleScope.launch(coroutineContext) {
|
||||
val streams = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
StreamsExtractor.extractStreams(videoId)
|
||||
MediaServiceRepository.instance.getStreams(videoId)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
toastFromMainDispatcher(getString(R.string.unknown_error))
|
||||
return@launch
|
||||
} catch (e: Exception) {
|
||||
toastFromMainDispatcher(
|
||||
StreamsExtractor.getExtractorErrorMessageString(this@DownloadService, e)
|
||||
)
|
||||
toastFromMainDispatcher(e.message ?: getString(R.string.server_error))
|
||||
return@launch
|
||||
}
|
||||
|
||||
@ -443,7 +444,7 @@ class DownloadService : LifecycleService() {
|
||||
*/
|
||||
private suspend fun regenerateLink(item: DownloadItem) {
|
||||
val streams = runCatching {
|
||||
StreamsExtractor.extractStreams(item.videoId)
|
||||
MediaServiceRepository.instance.getStreams(item.videoId)
|
||||
}.getOrNull() ?: return
|
||||
val stream = when (item.type) {
|
||||
FileType.AUDIO -> streams.audioStreams
|
||||
|
@ -13,7 +13,6 @@ import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.JsonHelper
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.SubscriptionHelper
|
||||
import com.github.libretube.api.obj.Segment
|
||||
import com.github.libretube.api.obj.Streams
|
||||
@ -37,6 +36,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Loads the selected videos audio in background mode with a notification area.
|
||||
@ -119,11 +119,12 @@ open class OnlinePlayerService : AbstractPlayerService() {
|
||||
|
||||
streams = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
StreamsExtractor.extractStreams(videoId)
|
||||
MediaServiceRepository.instance.getStreams(videoId)
|
||||
} catch (e: IOException) {
|
||||
toastFromMainDispatcher(getString(R.string.unknown_error))
|
||||
return@withContext null
|
||||
} catch (e: Exception) {
|
||||
val errorMessage =
|
||||
StreamsExtractor.getExtractorErrorMessageString(this@OnlinePlayerService, e)
|
||||
this@OnlinePlayerService.toastFromMainDispatcher(errorMessage)
|
||||
toastFromMainDispatcher(e.message ?: getString(R.string.server_error))
|
||||
return@withContext null
|
||||
}
|
||||
} ?: return
|
||||
|
@ -12,7 +12,6 @@ import com.github.libretube.LibreTubeApp.Companion.PLAYLIST_DOWNLOAD_ENQUEUE_CHA
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.PlaylistsHelper
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.obj.PipedStream
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.constants.IntentData
|
||||
@ -137,7 +136,7 @@ class PlaylistDownloadEnqueueService : LifecycleService() {
|
||||
|
||||
for (stream in streams) {
|
||||
val videoInfo = runCatching {
|
||||
StreamsExtractor.extractStreams(stream.url!!.toID())
|
||||
MediaServiceRepository.instance.getStreams(stream.url!!.toID())
|
||||
}.getOrNull() ?: continue
|
||||
|
||||
val videoStream = getStream(videoInfo.videoStreams, maxVideoQuality)
|
||||
|
@ -6,7 +6,7 @@ import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
||||
@ -41,7 +41,7 @@ class AddToPlaylistActivity : BaseActivity() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val videoInfo = if (PreferenceHelper.getToken().isEmpty()) {
|
||||
try {
|
||||
StreamsExtractor.extractStreams(videoId).toStreamItem(videoId)
|
||||
MediaServiceRepository.instance.getStreams(videoId).toStreamItem(videoId)
|
||||
} catch (e: Exception) {
|
||||
toastFromMainDispatcher(R.string.unknown_error)
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -159,12 +159,10 @@ class SearchChannelAdapter : ListAdapter<ContentItem, SearchViewHolder>(
|
||||
}
|
||||
|
||||
root.setOnLongClickListener {
|
||||
val playlistId = item.url.toID()
|
||||
val playlistName = item.name!!
|
||||
val sheet = PlaylistOptionsBottomSheet()
|
||||
sheet.arguments = bundleOf(
|
||||
IntentData.playlistId to playlistId,
|
||||
IntentData.playlistName to playlistName,
|
||||
IntentData.playlistId to item.url.toID(),
|
||||
IntentData.playlistName to item.name.orEmpty(),
|
||||
IntentData.playlistType to PlaylistType.PUBLIC
|
||||
)
|
||||
sheet.show(
|
||||
|
@ -5,7 +5,6 @@ import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isGone
|
||||
@ -13,13 +12,12 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.PipedStream
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.api.obj.Subtitle
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.databinding.DialogDownloadBinding
|
||||
import com.github.libretube.extensions.TAG
|
||||
import com.github.libretube.extensions.getWhileDigit
|
||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
||||
import com.github.libretube.helpers.DownloadHelper
|
||||
@ -30,6 +28,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
|
||||
class DownloadDialog : DialogFragment() {
|
||||
private lateinit var videoId: String
|
||||
@ -80,13 +79,13 @@ class DownloadDialog : DialogFragment() {
|
||||
lifecycleScope.launch {
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
StreamsExtractor.extractStreams(videoId)
|
||||
MediaServiceRepository.instance.getStreams(videoId)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
context?.toastFromMainDispatcher(getString(R.string.unknown_error))
|
||||
return@launch
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG(), e.stackTraceToString())
|
||||
val context = context ?: return@launch
|
||||
val errorMessage = StreamsExtractor.getExtractorErrorMessageString(context, e)
|
||||
context.toastFromMainDispatcher(errorMessage)
|
||||
context?.toastFromMainDispatcher(e.message ?: getString(R.string.server_error))
|
||||
return@launch
|
||||
}
|
||||
initDownloadOptions(binding, response)
|
||||
|
@ -4,6 +4,8 @@ import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.Comment
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class CommentPagingSource(
|
||||
private val videoId: String,
|
||||
@ -13,9 +15,11 @@ class CommentPagingSource(
|
||||
|
||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, Comment> {
|
||||
return try {
|
||||
val result = params.key?.let {
|
||||
MediaServiceRepository.instance.getCommentsNextPage(videoId, it)
|
||||
} ?: MediaServiceRepository.instance.getComments(videoId)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
params.key?.let {
|
||||
MediaServiceRepository.instance.getCommentsNextPage(videoId, it)
|
||||
} ?: MediaServiceRepository.instance.getComments(videoId)
|
||||
}
|
||||
|
||||
if (result.commentCount > 0) onCommentCount(result.commentCount)
|
||||
|
||||
|
@ -4,6 +4,8 @@ import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.Comment
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class CommentRepliesPagingSource(
|
||||
private val videoId: String,
|
||||
@ -14,7 +16,9 @@ class CommentRepliesPagingSource(
|
||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, Comment> {
|
||||
return try {
|
||||
val key = params.key.orEmpty().ifEmpty { originalComment.repliesPage.orEmpty() }
|
||||
val result = MediaServiceRepository.instance.getCommentsNextPage(videoId, key)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
MediaServiceRepository.instance.getCommentsNextPage(videoId, key)
|
||||
}
|
||||
|
||||
val replies = result.comments.toMutableList()
|
||||
if (params.key.isNullOrEmpty()) {
|
||||
|
@ -5,6 +5,8 @@ import androidx.paging.PagingState
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.ContentItem
|
||||
import com.github.libretube.util.deArrow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SearchPagingSource(
|
||||
private val searchQuery: String,
|
||||
@ -14,9 +16,14 @@ class SearchPagingSource(
|
||||
|
||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, ContentItem> {
|
||||
return try {
|
||||
val result = params.key?.let {
|
||||
MediaServiceRepository.instance.getSearchResultsNextPage(searchQuery, searchFilter, it)
|
||||
} ?: MediaServiceRepository.instance.getSearchResults(searchQuery, searchFilter)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
params.key?.let {
|
||||
MediaServiceRepository.instance.getSearchResultsNextPage(
|
||||
searchQuery, searchFilter, it
|
||||
)
|
||||
} ?: MediaServiceRepository.instance.getSearchResults(searchQuery, searchFilter)
|
||||
}
|
||||
|
||||
LoadResult.Page(result.items.deArrow(), null, result.nextpage)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
|
@ -5,6 +5,7 @@ import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.ContentItem
|
||||
import com.github.libretube.api.obj.DeArrowContent
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.api.obj.StreamItem.Companion.TYPE_STREAM
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.extensions.toID
|
||||
@ -89,7 +90,7 @@ object DeArrowUtil {
|
||||
if (!PreferenceHelper.getBoolean(PreferenceKeys.DEARROW, false)) return contentItems
|
||||
|
||||
val videoIds = contentItems
|
||||
.filter { it.type == "stream" }
|
||||
.filter { it.type == TYPE_STREAM }
|
||||
.map { it.url.toID() }
|
||||
|
||||
if (videoIds.isEmpty()) return contentItems
|
||||
|
@ -3,8 +3,6 @@ package com.github.libretube.util
|
||||
import androidx.media3.common.Player
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.PlaylistsHelper
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.extensions.move
|
||||
import com.github.libretube.extensions.runCatchingIO
|
||||
@ -194,7 +192,7 @@ object PlayingQueue {
|
||||
}.let { queueJobs.add(it) }
|
||||
|
||||
fun insertByVideoId(videoId: String) = runCatchingIO {
|
||||
val streams = StreamsExtractor.extractStreams(videoId.toID())
|
||||
val streams = MediaServiceRepository.instance.getStreams(videoId.toID())
|
||||
add(streams.toStreamItem(videoId))
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user