mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-28 07:50: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.SegmentData
|
||||||
import com.github.libretube.api.obj.StreamItem
|
import com.github.libretube.api.obj.StreamItem
|
||||||
import com.github.libretube.api.obj.Streams
|
import com.github.libretube.api.obj.Streams
|
||||||
|
import com.github.libretube.helpers.PlayerHelper
|
||||||
|
|
||||||
interface MediaServiceRepository {
|
interface MediaServiceRepository {
|
||||||
suspend fun getTrending(region: String): List<StreamItem>
|
suspend fun getTrending(region: String): List<StreamItem>
|
||||||
suspend fun getStreams(videoId: String): Streams
|
suspend fun getStreams(videoId: String): Streams
|
||||||
suspend fun getComments(videoId: String): CommentsPage
|
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 getDeArrowContent(videoIds: String): Map<String, DeArrowContent>
|
||||||
suspend fun getCommentsNextPage(videoId: String, nextPage: String): CommentsPage
|
suspend fun getCommentsNextPage(videoId: String, nextPage: String): CommentsPage
|
||||||
suspend fun getSearchResults(searchQuery: String, filter: String): SearchResult
|
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 getSuggestions(query: String): List<String>
|
||||||
suspend fun getChannel(channelId: String): Channel
|
suspend fun getChannel(channelId: String): Channel
|
||||||
suspend fun getChannelTab(data: String, nextPage: String? = null): ChannelTabResponse
|
suspend fun getChannelTab(data: String, nextPage: String? = null): ChannelTabResponse
|
||||||
@ -29,7 +40,12 @@ interface MediaServiceRepository {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance by lazy {
|
val instance by lazy {
|
||||||
|
if (PlayerHelper.disablePipedProxy && PlayerHelper.localStreamExtraction) {
|
||||||
|
// TODO: LocalStreamsExtractionPipedMediaServiceRepository()
|
||||||
|
NewPipeMediaServiceRepository()
|
||||||
|
} else {
|
||||||
PipedMediaServiceRepository()
|
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.ChannelTabResponse
|
||||||
import com.github.libretube.api.obj.CommentsPage
|
import com.github.libretube.api.obj.CommentsPage
|
||||||
import com.github.libretube.api.obj.DeArrowContent
|
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.Playlist
|
||||||
import com.github.libretube.api.obj.SearchResult
|
import com.github.libretube.api.obj.SearchResult
|
||||||
import com.github.libretube.api.obj.SegmentData
|
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.api.obj.Streams
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
|
import retrofit2.HttpException
|
||||||
|
|
||||||
class PipedMediaServiceRepository : MediaServiceRepository {
|
open class PipedMediaServiceRepository : MediaServiceRepository {
|
||||||
override suspend fun getTrending(region: String): List<StreamItem> =
|
override suspend fun getTrending(region: String): List<StreamItem> =
|
||||||
api.getTrending(region)
|
api.getTrending(region)
|
||||||
|
|
||||||
override suspend fun getStreams(videoId: String): Streams =
|
override suspend fun getStreams(videoId: String): Streams {
|
||||||
|
return try {
|
||||||
api.getStreams(videoId)
|
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 =
|
override suspend fun getComments(videoId: String): CommentsPage =
|
||||||
api.getComments(videoId)
|
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 avatarUrl: String? = null,
|
||||||
val bannerUrl: String? = null,
|
val bannerUrl: String? = null,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val nextpage: String? = null,
|
var nextpage: String? = null,
|
||||||
val subscriberCount: Long = 0,
|
val subscriberCount: Long = 0,
|
||||||
val verified: Boolean = false,
|
val verified: Boolean = false,
|
||||||
var relatedStreams: List<StreamItem> = emptyList(),
|
var relatedStreams: List<StreamItem> = emptyList(),
|
||||||
|
@ -6,6 +6,4 @@ import kotlinx.serialization.Serializable
|
|||||||
data class SearchResult(
|
data class SearchResult(
|
||||||
var items: List<ContentItem> = emptyList(),
|
var items: List<ContentItem> = emptyList(),
|
||||||
val nextpage: String? = null,
|
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.MediaServiceRepository
|
||||||
import com.github.libretube.api.PlaylistsHelper
|
import com.github.libretube.api.PlaylistsHelper
|
||||||
import com.github.libretube.api.PlaylistsHelper.MAX_CONCURRENT_IMPORT_CALLS
|
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.Playlist
|
||||||
import com.github.libretube.api.obj.Playlists
|
import com.github.libretube.api.obj.Playlists
|
||||||
import com.github.libretube.api.obj.StreamItem
|
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
|
// 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)) {
|
for (videoIdList in playlist.videos.chunked(MAX_CONCURRENT_IMPORT_CALLS)) {
|
||||||
val streams = videoIdList.parallelMap {
|
val streams = videoIdList.parallelMap {
|
||||||
runCatching { StreamsExtractor.extractStreams(it) }
|
runCatching { MediaServiceRepository.instance.getStreams(it) }
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
?.toStreamItem(it)
|
?.toStreamItem(it)
|
||||||
}.filterNotNull()
|
}.filterNotNull()
|
||||||
|
@ -20,7 +20,7 @@ import androidx.lifecycle.LifecycleService
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.libretube.LibreTubeApp.Companion.DOWNLOAD_CHANNEL_NAME
|
import com.github.libretube.LibreTubeApp.Companion.DOWNLOAD_CHANNEL_NAME
|
||||||
import com.github.libretube.R
|
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.api.obj.Streams
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.db.DatabaseHolder.Database
|
import com.github.libretube.db.DatabaseHolder.Database
|
||||||
@ -124,12 +124,13 @@ class DownloadService : LifecycleService() {
|
|||||||
lifecycleScope.launch(coroutineContext) {
|
lifecycleScope.launch(coroutineContext) {
|
||||||
val streams = try {
|
val streams = try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
StreamsExtractor.extractStreams(videoId)
|
MediaServiceRepository.instance.getStreams(videoId)
|
||||||
}
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
toastFromMainDispatcher(getString(R.string.unknown_error))
|
||||||
|
return@launch
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
toastFromMainDispatcher(
|
toastFromMainDispatcher(e.message ?: getString(R.string.server_error))
|
||||||
StreamsExtractor.getExtractorErrorMessageString(this@DownloadService, e)
|
|
||||||
)
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,7 +444,7 @@ class DownloadService : LifecycleService() {
|
|||||||
*/
|
*/
|
||||||
private suspend fun regenerateLink(item: DownloadItem) {
|
private suspend fun regenerateLink(item: DownloadItem) {
|
||||||
val streams = runCatching {
|
val streams = runCatching {
|
||||||
StreamsExtractor.extractStreams(item.videoId)
|
MediaServiceRepository.instance.getStreams(item.videoId)
|
||||||
}.getOrNull() ?: return
|
}.getOrNull() ?: return
|
||||||
val stream = when (item.type) {
|
val stream = when (item.type) {
|
||||||
FileType.AUDIO -> streams.audioStreams
|
FileType.AUDIO -> streams.audioStreams
|
||||||
|
@ -13,7 +13,6 @@ import androidx.media3.exoplayer.hls.HlsMediaSource
|
|||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.JsonHelper
|
import com.github.libretube.api.JsonHelper
|
||||||
import com.github.libretube.api.MediaServiceRepository
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.api.StreamsExtractor
|
|
||||||
import com.github.libretube.api.SubscriptionHelper
|
import com.github.libretube.api.SubscriptionHelper
|
||||||
import com.github.libretube.api.obj.Segment
|
import com.github.libretube.api.obj.Segment
|
||||||
import com.github.libretube.api.obj.Streams
|
import com.github.libretube.api.obj.Streams
|
||||||
@ -37,6 +36,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the selected videos audio in background mode with a notification area.
|
* Loads the selected videos audio in background mode with a notification area.
|
||||||
@ -119,11 +119,12 @@ open class OnlinePlayerService : AbstractPlayerService() {
|
|||||||
|
|
||||||
streams = withContext(Dispatchers.IO) {
|
streams = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
StreamsExtractor.extractStreams(videoId)
|
MediaServiceRepository.instance.getStreams(videoId)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
toastFromMainDispatcher(getString(R.string.unknown_error))
|
||||||
|
return@withContext null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val errorMessage =
|
toastFromMainDispatcher(e.message ?: getString(R.string.server_error))
|
||||||
StreamsExtractor.getExtractorErrorMessageString(this@OnlinePlayerService, e)
|
|
||||||
this@OnlinePlayerService.toastFromMainDispatcher(errorMessage)
|
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
} ?: return
|
} ?: return
|
||||||
|
@ -12,7 +12,6 @@ import com.github.libretube.LibreTubeApp.Companion.PLAYLIST_DOWNLOAD_ENQUEUE_CHA
|
|||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.MediaServiceRepository
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.api.PlaylistsHelper
|
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.PipedStream
|
||||||
import com.github.libretube.api.obj.StreamItem
|
import com.github.libretube.api.obj.StreamItem
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
@ -137,7 +136,7 @@ class PlaylistDownloadEnqueueService : LifecycleService() {
|
|||||||
|
|
||||||
for (stream in streams) {
|
for (stream in streams) {
|
||||||
val videoInfo = runCatching {
|
val videoInfo = runCatching {
|
||||||
StreamsExtractor.extractStreams(stream.url!!.toID())
|
MediaServiceRepository.instance.getStreams(stream.url!!.toID())
|
||||||
}.getOrNull() ?: continue
|
}.getOrNull() ?: continue
|
||||||
|
|
||||||
val videoStream = getStream(videoInfo.videoStreams, maxVideoQuality)
|
val videoStream = getStream(videoInfo.videoStreams, maxVideoQuality)
|
||||||
|
@ -6,7 +6,7 @@ import androidx.core.net.toUri
|
|||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.libretube.R
|
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.api.obj.StreamItem
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
import com.github.libretube.extensions.toastFromMainDispatcher
|
||||||
@ -41,7 +41,7 @@ class AddToPlaylistActivity : BaseActivity() {
|
|||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val videoInfo = if (PreferenceHelper.getToken().isEmpty()) {
|
val videoInfo = if (PreferenceHelper.getToken().isEmpty()) {
|
||||||
try {
|
try {
|
||||||
StreamsExtractor.extractStreams(videoId).toStreamItem(videoId)
|
MediaServiceRepository.instance.getStreams(videoId).toStreamItem(videoId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
toastFromMainDispatcher(R.string.unknown_error)
|
toastFromMainDispatcher(R.string.unknown_error)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
@ -159,12 +159,10 @@ class SearchChannelAdapter : ListAdapter<ContentItem, SearchViewHolder>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
root.setOnLongClickListener {
|
root.setOnLongClickListener {
|
||||||
val playlistId = item.url.toID()
|
|
||||||
val playlistName = item.name!!
|
|
||||||
val sheet = PlaylistOptionsBottomSheet()
|
val sheet = PlaylistOptionsBottomSheet()
|
||||||
sheet.arguments = bundleOf(
|
sheet.arguments = bundleOf(
|
||||||
IntentData.playlistId to playlistId,
|
IntentData.playlistId to item.url.toID(),
|
||||||
IntentData.playlistName to playlistName,
|
IntentData.playlistName to item.name.orEmpty(),
|
||||||
IntentData.playlistType to PlaylistType.PUBLIC
|
IntentData.playlistType to PlaylistType.PUBLIC
|
||||||
)
|
)
|
||||||
sheet.show(
|
sheet.show(
|
||||||
|
@ -5,7 +5,6 @@ import android.content.DialogInterface
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
@ -13,13 +12,12 @@ import androidx.fragment.app.DialogFragment
|
|||||||
import androidx.fragment.app.setFragmentResult
|
import androidx.fragment.app.setFragmentResult
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.libretube.R
|
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.PipedStream
|
||||||
import com.github.libretube.api.obj.Streams
|
import com.github.libretube.api.obj.Streams
|
||||||
import com.github.libretube.api.obj.Subtitle
|
import com.github.libretube.api.obj.Subtitle
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.databinding.DialogDownloadBinding
|
import com.github.libretube.databinding.DialogDownloadBinding
|
||||||
import com.github.libretube.extensions.TAG
|
|
||||||
import com.github.libretube.extensions.getWhileDigit
|
import com.github.libretube.extensions.getWhileDigit
|
||||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
import com.github.libretube.extensions.toastFromMainDispatcher
|
||||||
import com.github.libretube.helpers.DownloadHelper
|
import com.github.libretube.helpers.DownloadHelper
|
||||||
@ -30,6 +28,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class DownloadDialog : DialogFragment() {
|
class DownloadDialog : DialogFragment() {
|
||||||
private lateinit var videoId: String
|
private lateinit var videoId: String
|
||||||
@ -80,13 +79,13 @@ class DownloadDialog : DialogFragment() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val response = try {
|
val response = try {
|
||||||
withContext(Dispatchers.IO) {
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG(), e.stackTraceToString())
|
context?.toastFromMainDispatcher(e.message ?: getString(R.string.server_error))
|
||||||
val context = context ?: return@launch
|
|
||||||
val errorMessage = StreamsExtractor.getExtractorErrorMessageString(context, e)
|
|
||||||
context.toastFromMainDispatcher(errorMessage)
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
initDownloadOptions(binding, response)
|
initDownloadOptions(binding, response)
|
||||||
|
@ -4,6 +4,8 @@ import androidx.paging.PagingSource
|
|||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import com.github.libretube.api.MediaServiceRepository
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.api.obj.Comment
|
import com.github.libretube.api.obj.Comment
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class CommentPagingSource(
|
class CommentPagingSource(
|
||||||
private val videoId: String,
|
private val videoId: String,
|
||||||
@ -13,9 +15,11 @@ class CommentPagingSource(
|
|||||||
|
|
||||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, Comment> {
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, Comment> {
|
||||||
return try {
|
return try {
|
||||||
val result = params.key?.let {
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
params.key?.let {
|
||||||
MediaServiceRepository.instance.getCommentsNextPage(videoId, it)
|
MediaServiceRepository.instance.getCommentsNextPage(videoId, it)
|
||||||
} ?: MediaServiceRepository.instance.getComments(videoId)
|
} ?: MediaServiceRepository.instance.getComments(videoId)
|
||||||
|
}
|
||||||
|
|
||||||
if (result.commentCount > 0) onCommentCount(result.commentCount)
|
if (result.commentCount > 0) onCommentCount(result.commentCount)
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ import androidx.paging.PagingSource
|
|||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import com.github.libretube.api.MediaServiceRepository
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.api.obj.Comment
|
import com.github.libretube.api.obj.Comment
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class CommentRepliesPagingSource(
|
class CommentRepliesPagingSource(
|
||||||
private val videoId: String,
|
private val videoId: String,
|
||||||
@ -14,7 +16,9 @@ class CommentRepliesPagingSource(
|
|||||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, Comment> {
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, Comment> {
|
||||||
return try {
|
return try {
|
||||||
val key = params.key.orEmpty().ifEmpty { originalComment.repliesPage.orEmpty() }
|
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()
|
val replies = result.comments.toMutableList()
|
||||||
if (params.key.isNullOrEmpty()) {
|
if (params.key.isNullOrEmpty()) {
|
||||||
|
@ -5,6 +5,8 @@ import androidx.paging.PagingState
|
|||||||
import com.github.libretube.api.MediaServiceRepository
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.api.obj.ContentItem
|
import com.github.libretube.api.obj.ContentItem
|
||||||
import com.github.libretube.util.deArrow
|
import com.github.libretube.util.deArrow
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class SearchPagingSource(
|
class SearchPagingSource(
|
||||||
private val searchQuery: String,
|
private val searchQuery: String,
|
||||||
@ -14,9 +16,14 @@ class SearchPagingSource(
|
|||||||
|
|
||||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, ContentItem> {
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, ContentItem> {
|
||||||
return try {
|
return try {
|
||||||
val result = params.key?.let {
|
val result = withContext(Dispatchers.IO) {
|
||||||
MediaServiceRepository.instance.getSearchResultsNextPage(searchQuery, searchFilter, it)
|
params.key?.let {
|
||||||
|
MediaServiceRepository.instance.getSearchResultsNextPage(
|
||||||
|
searchQuery, searchFilter, it
|
||||||
|
)
|
||||||
} ?: MediaServiceRepository.instance.getSearchResults(searchQuery, searchFilter)
|
} ?: MediaServiceRepository.instance.getSearchResults(searchQuery, searchFilter)
|
||||||
|
}
|
||||||
|
|
||||||
LoadResult.Page(result.items.deArrow(), null, result.nextpage)
|
LoadResult.Page(result.items.deArrow(), null, result.nextpage)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LoadResult.Error(e)
|
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.ContentItem
|
||||||
import com.github.libretube.api.obj.DeArrowContent
|
import com.github.libretube.api.obj.DeArrowContent
|
||||||
import com.github.libretube.api.obj.StreamItem
|
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.api.obj.Streams
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
@ -89,7 +90,7 @@ object DeArrowUtil {
|
|||||||
if (!PreferenceHelper.getBoolean(PreferenceKeys.DEARROW, false)) return contentItems
|
if (!PreferenceHelper.getBoolean(PreferenceKeys.DEARROW, false)) return contentItems
|
||||||
|
|
||||||
val videoIds = contentItems
|
val videoIds = contentItems
|
||||||
.filter { it.type == "stream" }
|
.filter { it.type == TYPE_STREAM }
|
||||||
.map { it.url.toID() }
|
.map { it.url.toID() }
|
||||||
|
|
||||||
if (videoIds.isEmpty()) return contentItems
|
if (videoIds.isEmpty()) return contentItems
|
||||||
|
@ -3,8 +3,6 @@ package com.github.libretube.util
|
|||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import com.github.libretube.api.MediaServiceRepository
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.api.PlaylistsHelper
|
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.api.obj.StreamItem
|
||||||
import com.github.libretube.extensions.move
|
import com.github.libretube.extensions.move
|
||||||
import com.github.libretube.extensions.runCatchingIO
|
import com.github.libretube.extensions.runCatchingIO
|
||||||
@ -194,7 +192,7 @@ object PlayingQueue {
|
|||||||
}.let { queueJobs.add(it) }
|
}.let { queueJobs.add(it) }
|
||||||
|
|
||||||
fun insertByVideoId(videoId: String) = runCatchingIO {
|
fun insertByVideoId(videoId: String) = runCatchingIO {
|
||||||
val streams = StreamsExtractor.extractStreams(videoId.toID())
|
val streams = MediaServiceRepository.instance.getStreams(videoId.toID())
|
||||||
add(streams.toStreamItem(videoId))
|
add(streams.toStreamItem(videoId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user