mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-27 23:40:33 +05:30
Merge pull request #7151 from Bnyro/local-npe
feat: add support for using local NewPipe Extractor
This commit is contained in:
commit
d7f874f5da
@ -1,6 +1,7 @@
|
|||||||
package com.github.libretube.api
|
package com.github.libretube.api
|
||||||
|
|
||||||
import com.github.libretube.api.obj.DeArrowBody
|
import com.github.libretube.api.obj.DeArrowBody
|
||||||
|
import com.github.libretube.api.obj.PipedConfig
|
||||||
import com.github.libretube.api.obj.PipedInstance
|
import com.github.libretube.api.obj.PipedInstance
|
||||||
import com.github.libretube.api.obj.SubmitSegmentResponse
|
import com.github.libretube.api.obj.SubmitSegmentResponse
|
||||||
import com.github.libretube.api.obj.VoteInfo
|
import com.github.libretube.api.obj.VoteInfo
|
||||||
@ -20,6 +21,9 @@ interface ExternalApi {
|
|||||||
@GET
|
@GET
|
||||||
suspend fun getInstances(@Url url: String): List<PipedInstance>
|
suspend fun getInstances(@Url url: String): List<PipedInstance>
|
||||||
|
|
||||||
|
@GET("config")
|
||||||
|
suspend fun getInstanceConfig(@Url url: String): PipedConfig
|
||||||
|
|
||||||
// fetch latest version info
|
// fetch latest version info
|
||||||
@GET(GITHUB_API_URL)
|
@GET(GITHUB_API_URL)
|
||||||
suspend fun getLatestRelease(): UpdateInfo
|
suspend fun getLatestRelease(): UpdateInfo
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package com.github.libretube.api
|
||||||
|
|
||||||
|
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.Playlist
|
||||||
|
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 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 getSuggestions(query: String): List<String>
|
||||||
|
suspend fun getChannel(channelId: String): Channel
|
||||||
|
suspend fun getChannelTab(data: String, nextPage: String? = null): ChannelTabResponse
|
||||||
|
suspend fun getChannelByName(channelName: String): Channel
|
||||||
|
suspend fun getChannelNextPage(channelId: String, nextPage: String): Channel
|
||||||
|
suspend fun getPlaylist(playlistId: String): Playlist
|
||||||
|
suspend fun getPlaylistNextPage(playlistId: String, nextPage: String): Playlist
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance: MediaServiceRepository
|
||||||
|
get() = when {
|
||||||
|
PlayerHelper.fullLocalMode -> NewPipeMediaServiceRepository()
|
||||||
|
PlayerHelper.localStreamExtraction -> LocalStreamsExtractionPipedMediaServiceRepository()
|
||||||
|
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() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -4,33 +4,16 @@ 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.DeleteUserRequest
|
|
||||||
import com.github.libretube.api.obj.EditPlaylistBody
|
|
||||||
import com.github.libretube.api.obj.Login
|
|
||||||
import com.github.libretube.api.obj.Message
|
|
||||||
import com.github.libretube.api.obj.PipedConfig
|
|
||||||
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.SearchResult
|
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.api.obj.Subscribe
|
|
||||||
import com.github.libretube.api.obj.Subscribed
|
|
||||||
import com.github.libretube.api.obj.Subscription
|
|
||||||
import com.github.libretube.api.obj.Token
|
|
||||||
import retrofit2.http.Body
|
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Header
|
|
||||||
import retrofit2.http.PATCH
|
|
||||||
import retrofit2.http.POST
|
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface PipedApi {
|
interface PipedApi {
|
||||||
@GET("config")
|
|
||||||
suspend fun getConfig(): PipedConfig
|
|
||||||
|
|
||||||
@GET("trending")
|
@GET("trending")
|
||||||
suspend fun getTrending(@Query("region") region: String): List<StreamItem>
|
suspend fun getTrending(@Query("region") region: String): List<StreamItem>
|
||||||
|
|
||||||
@ -98,106 +81,4 @@ interface PipedApi {
|
|||||||
@Path("playlistId") playlistId: String,
|
@Path("playlistId") playlistId: String,
|
||||||
@Query("nextpage") nextPage: String
|
@Query("nextpage") nextPage: String
|
||||||
): Playlist
|
): Playlist
|
||||||
|
|
||||||
@POST("login")
|
|
||||||
suspend fun login(@Body login: Login): Token
|
|
||||||
|
|
||||||
@POST("register")
|
|
||||||
suspend fun register(@Body login: Login): Token
|
|
||||||
|
|
||||||
@POST("user/delete")
|
|
||||||
suspend fun deleteAccount(
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body password: DeleteUserRequest
|
|
||||||
)
|
|
||||||
|
|
||||||
@GET("feed")
|
|
||||||
suspend fun getFeed(@Query("authToken") token: String?): List<StreamItem>
|
|
||||||
|
|
||||||
@GET("feed/unauthenticated")
|
|
||||||
suspend fun getUnauthenticatedFeed(@Query("channels") channels: String): List<StreamItem>
|
|
||||||
|
|
||||||
@POST("feed/unauthenticated")
|
|
||||||
suspend fun getUnauthenticatedFeed(@Body channels: List<String>): List<StreamItem>
|
|
||||||
|
|
||||||
@GET("subscribed")
|
|
||||||
suspend fun isSubscribed(
|
|
||||||
@Query("channelId") channelId: String,
|
|
||||||
@Header("Authorization") token: String
|
|
||||||
): Subscribed
|
|
||||||
|
|
||||||
@GET("subscriptions")
|
|
||||||
suspend fun subscriptions(@Header("Authorization") token: String): List<Subscription>
|
|
||||||
|
|
||||||
@GET("subscriptions/unauthenticated")
|
|
||||||
suspend fun unauthenticatedSubscriptions(
|
|
||||||
@Query("channels") channels: String
|
|
||||||
): List<Subscription>
|
|
||||||
|
|
||||||
@POST("subscriptions/unauthenticated")
|
|
||||||
suspend fun unauthenticatedSubscriptions(@Body channels: List<String>): List<Subscription>
|
|
||||||
|
|
||||||
@POST("subscribe")
|
|
||||||
suspend fun subscribe(
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body subscribe: Subscribe
|
|
||||||
): Message
|
|
||||||
|
|
||||||
@POST("unsubscribe")
|
|
||||||
suspend fun unsubscribe(
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body subscribe: Subscribe
|
|
||||||
): Message
|
|
||||||
|
|
||||||
@POST("import")
|
|
||||||
suspend fun importSubscriptions(
|
|
||||||
@Query("override") override: Boolean,
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body channels: List<String>
|
|
||||||
): Message
|
|
||||||
|
|
||||||
@POST("import/playlist")
|
|
||||||
suspend fun clonePlaylist(
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body editPlaylistBody: EditPlaylistBody
|
|
||||||
): EditPlaylistBody
|
|
||||||
|
|
||||||
@GET("user/playlists")
|
|
||||||
suspend fun getUserPlaylists(@Header("Authorization") token: String): List<Playlists>
|
|
||||||
|
|
||||||
@POST("user/playlists/rename")
|
|
||||||
suspend fun renamePlaylist(
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body editPlaylistBody: EditPlaylistBody
|
|
||||||
): Message
|
|
||||||
|
|
||||||
@PATCH("user/playlists/description")
|
|
||||||
suspend fun changePlaylistDescription(
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body editPlaylistBody: EditPlaylistBody
|
|
||||||
): Message
|
|
||||||
|
|
||||||
@POST("user/playlists/delete")
|
|
||||||
suspend fun deletePlaylist(
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body editPlaylistBody: EditPlaylistBody
|
|
||||||
): Message
|
|
||||||
|
|
||||||
@POST("user/playlists/create")
|
|
||||||
suspend fun createPlaylist(
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body name: Playlists
|
|
||||||
): EditPlaylistBody
|
|
||||||
|
|
||||||
@POST("user/playlists/add")
|
|
||||||
suspend fun addToPlaylist(
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body editPlaylistBody: EditPlaylistBody
|
|
||||||
): Message
|
|
||||||
|
|
||||||
@POST("user/playlists/remove")
|
|
||||||
suspend fun removeFromPlaylist(
|
|
||||||
@Header("Authorization") token: String,
|
|
||||||
@Body editPlaylistBody: EditPlaylistBody
|
|
||||||
): Message
|
|
||||||
}
|
}
|
||||||
|
128
app/src/main/java/com/github/libretube/api/PipedAuthApi.kt
Normal file
128
app/src/main/java/com/github/libretube/api/PipedAuthApi.kt
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package com.github.libretube.api
|
||||||
|
|
||||||
|
import com.github.libretube.api.obj.DeleteUserRequest
|
||||||
|
import com.github.libretube.api.obj.EditPlaylistBody
|
||||||
|
import com.github.libretube.api.obj.Login
|
||||||
|
import com.github.libretube.api.obj.Message
|
||||||
|
import com.github.libretube.api.obj.Playlist
|
||||||
|
import com.github.libretube.api.obj.Playlists
|
||||||
|
import com.github.libretube.api.obj.StreamItem
|
||||||
|
import com.github.libretube.api.obj.Subscribe
|
||||||
|
import com.github.libretube.api.obj.Subscribed
|
||||||
|
import com.github.libretube.api.obj.Subscription
|
||||||
|
import com.github.libretube.api.obj.Token
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Header
|
||||||
|
import retrofit2.http.PATCH
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
interface PipedAuthApi {
|
||||||
|
|
||||||
|
@POST("login")
|
||||||
|
suspend fun login(@Body login: Login): Token
|
||||||
|
|
||||||
|
@POST("register")
|
||||||
|
suspend fun register(@Body login: Login): Token
|
||||||
|
|
||||||
|
@POST("user/delete")
|
||||||
|
suspend fun deleteAccount(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body password: DeleteUserRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
@GET("feed")
|
||||||
|
suspend fun getFeed(@Query("authToken") token: String?): List<StreamItem>
|
||||||
|
|
||||||
|
@GET("feed/unauthenticated")
|
||||||
|
suspend fun getUnauthenticatedFeed(@Query("channels") channels: String): List<StreamItem>
|
||||||
|
|
||||||
|
@POST("feed/unauthenticated")
|
||||||
|
suspend fun getUnauthenticatedFeed(@Body channels: List<String>): List<StreamItem>
|
||||||
|
|
||||||
|
@GET("subscribed")
|
||||||
|
suspend fun isSubscribed(
|
||||||
|
@Query("channelId") channelId: String,
|
||||||
|
@Header("Authorization") token: String
|
||||||
|
): Subscribed
|
||||||
|
|
||||||
|
@GET("subscriptions")
|
||||||
|
suspend fun subscriptions(@Header("Authorization") token: String): List<Subscription>
|
||||||
|
|
||||||
|
@GET("subscriptions/unauthenticated")
|
||||||
|
suspend fun unauthenticatedSubscriptions(
|
||||||
|
@Query("channels") channels: String
|
||||||
|
): List<Subscription>
|
||||||
|
|
||||||
|
@POST("subscriptions/unauthenticated")
|
||||||
|
suspend fun unauthenticatedSubscriptions(@Body channels: List<String>): List<Subscription>
|
||||||
|
|
||||||
|
@POST("subscribe")
|
||||||
|
suspend fun subscribe(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body subscribe: Subscribe
|
||||||
|
): Message
|
||||||
|
|
||||||
|
@POST("unsubscribe")
|
||||||
|
suspend fun unsubscribe(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body subscribe: Subscribe
|
||||||
|
): Message
|
||||||
|
|
||||||
|
@POST("import")
|
||||||
|
suspend fun importSubscriptions(
|
||||||
|
@Query("override") override: Boolean,
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body channels: List<String>
|
||||||
|
): Message
|
||||||
|
|
||||||
|
@POST("import/playlist")
|
||||||
|
suspend fun clonePlaylist(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body editPlaylistBody: EditPlaylistBody
|
||||||
|
): EditPlaylistBody
|
||||||
|
|
||||||
|
@GET("user/playlists")
|
||||||
|
suspend fun getUserPlaylists(@Header("Authorization") token: String): List<Playlists>
|
||||||
|
|
||||||
|
@POST("user/playlists/rename")
|
||||||
|
suspend fun renamePlaylist(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body editPlaylistBody: EditPlaylistBody
|
||||||
|
): Message
|
||||||
|
|
||||||
|
@PATCH("user/playlists/description")
|
||||||
|
suspend fun changePlaylistDescription(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body editPlaylistBody: EditPlaylistBody
|
||||||
|
): Message
|
||||||
|
|
||||||
|
@POST("user/playlists/delete")
|
||||||
|
suspend fun deletePlaylist(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body editPlaylistBody: EditPlaylistBody
|
||||||
|
): Message
|
||||||
|
|
||||||
|
@POST("user/playlists/create")
|
||||||
|
suspend fun createPlaylist(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body name: Playlists
|
||||||
|
): EditPlaylistBody
|
||||||
|
|
||||||
|
@POST("user/playlists/add")
|
||||||
|
suspend fun addToPlaylist(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body editPlaylistBody: EditPlaylistBody
|
||||||
|
): Message
|
||||||
|
|
||||||
|
@POST("user/playlists/remove")
|
||||||
|
suspend fun removeFromPlaylist(
|
||||||
|
@Header("Authorization") token: String,
|
||||||
|
@Body editPlaylistBody: EditPlaylistBody
|
||||||
|
): Message
|
||||||
|
|
||||||
|
@GET("playlists/{playlistId}")
|
||||||
|
suspend fun getPlaylist(@Path("playlistId") playlistId: String): Playlist
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
package com.github.libretube.api
|
||||||
|
|
||||||
|
import com.github.libretube.api.RetrofitInstance.PIPED_API_URL
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
open class PipedMediaServiceRepository : MediaServiceRepository {
|
||||||
|
override suspend fun getTrending(region: String): List<StreamItem> =
|
||||||
|
api.getTrending(region)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
override suspend fun getSegments(
|
||||||
|
videoId: String,
|
||||||
|
category: String,
|
||||||
|
actionType: String?
|
||||||
|
): SegmentData = api.getSegments(videoId, category, actionType)
|
||||||
|
|
||||||
|
override suspend fun getDeArrowContent(videoIds: String): Map<String, DeArrowContent> =
|
||||||
|
api.getDeArrowContent(videoIds)
|
||||||
|
|
||||||
|
override suspend fun getCommentsNextPage(videoId: String, nextPage: String): CommentsPage =
|
||||||
|
api.getCommentsNextPage(videoId, nextPage)
|
||||||
|
|
||||||
|
override suspend fun getSearchResults(searchQuery: String, filter: String): SearchResult =
|
||||||
|
api.getSearchResults(searchQuery, filter)
|
||||||
|
|
||||||
|
override suspend fun getSearchResultsNextPage(
|
||||||
|
searchQuery: String,
|
||||||
|
filter: String,
|
||||||
|
nextPage: String
|
||||||
|
): SearchResult = api.getSearchResultsNextPage(searchQuery, filter, nextPage)
|
||||||
|
|
||||||
|
override suspend fun getSuggestions(query: String): List<String> =
|
||||||
|
api.getSuggestions(query)
|
||||||
|
|
||||||
|
override suspend fun getChannel(channelId: String): Channel =
|
||||||
|
api.getChannel(channelId)
|
||||||
|
|
||||||
|
override suspend fun getChannelTab(data: String, nextPage: String?): ChannelTabResponse =
|
||||||
|
api.getChannelTab(data, nextPage)
|
||||||
|
|
||||||
|
override suspend fun getChannelByName(channelName: String): Channel =
|
||||||
|
api.getChannelByName(channelName)
|
||||||
|
|
||||||
|
override suspend fun getChannelNextPage(channelId: String, nextPage: String): Channel =
|
||||||
|
api.getChannelNextPage(channelId, nextPage)
|
||||||
|
|
||||||
|
override suspend fun getPlaylist(playlistId: String): Playlist =
|
||||||
|
api.getPlaylist(playlistId)
|
||||||
|
|
||||||
|
override suspend fun getPlaylistNextPage(playlistId: String, nextPage: String): Playlist =
|
||||||
|
api.getPlaylistNextPage(playlistId, nextPage)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val apiUrl get() = PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, PIPED_API_URL)
|
||||||
|
|
||||||
|
private val api by resettableLazy(RetrofitInstance.apiLazyMgr) {
|
||||||
|
RetrofitInstance.buildRetrofitInstance<PipedApi>(apiUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -52,7 +52,7 @@ object PlaylistsHelper {
|
|||||||
suspend fun getPlaylist(playlistId: String): Playlist {
|
suspend fun getPlaylist(playlistId: String): Playlist {
|
||||||
// load locally stored playlists with the auth api
|
// load locally stored playlists with the auth api
|
||||||
return when (getPrivatePlaylistType(playlistId)) {
|
return when (getPrivatePlaylistType(playlistId)) {
|
||||||
PlaylistType.PUBLIC -> RetrofitInstance.api.getPlaylist(playlistId)
|
PlaylistType.PUBLIC -> MediaServiceRepository.instance.getPlaylist(playlistId)
|
||||||
else -> playlistsRepository.getPlaylist(playlistId)
|
else -> playlistsRepository.getPlaylist(playlistId)
|
||||||
}.apply {
|
}.apply {
|
||||||
relatedStreams = relatedStreams.deArrow()
|
relatedStreams = relatedStreams.deArrow()
|
||||||
|
@ -11,55 +11,33 @@ import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
|||||||
import retrofit2.create
|
import retrofit2.create
|
||||||
|
|
||||||
object RetrofitInstance {
|
object RetrofitInstance {
|
||||||
private const val PIPED_API_URL = "https://pipedapi.kavin.rocks"
|
const val PIPED_API_URL = "https://pipedapi.kavin.rocks"
|
||||||
val apiUrl get() = PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, PIPED_API_URL)
|
|
||||||
val authUrl
|
val authUrl
|
||||||
get() = when (
|
get() = if (
|
||||||
PreferenceHelper.getBoolean(
|
PreferenceHelper.getBoolean(
|
||||||
PreferenceKeys.AUTH_INSTANCE_TOGGLE,
|
PreferenceKeys.AUTH_INSTANCE_TOGGLE,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
true -> PreferenceHelper.getString(
|
PreferenceHelper.getString(
|
||||||
PreferenceKeys.AUTH_INSTANCE,
|
PreferenceKeys.AUTH_INSTANCE,
|
||||||
PIPED_API_URL
|
PIPED_API_URL
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
false -> apiUrl
|
PipedMediaServiceRepository.apiUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
val lazyMgr = resettableManager()
|
val apiLazyMgr = resettableManager()
|
||||||
private val kotlinxConverterFactory = JsonHelper.json
|
val kotlinxConverterFactory = JsonHelper.json
|
||||||
.asConverterFactory("application/json".toMediaType())
|
.asConverterFactory("application/json".toMediaType())
|
||||||
|
|
||||||
private val httpClient by lazy { buildClient() }
|
val httpClient by lazy { buildClient() }
|
||||||
|
|
||||||
val api by resettableLazy(lazyMgr) {
|
val authApi = buildRetrofitInstance<PipedAuthApi>(authUrl)
|
||||||
Retrofit.Builder()
|
|
||||||
.baseUrl(apiUrl)
|
|
||||||
.client(httpClient)
|
|
||||||
.addConverterFactory(kotlinxConverterFactory)
|
|
||||||
.build()
|
|
||||||
.create<PipedApi>()
|
|
||||||
}
|
|
||||||
|
|
||||||
val authApi by resettableLazy(lazyMgr) {
|
// the url provided here isn't actually used anywhere in the external api
|
||||||
Retrofit.Builder()
|
val externalApi = buildRetrofitInstance<ExternalApi>(PIPED_API_URL)
|
||||||
.baseUrl(authUrl)
|
|
||||||
.client(httpClient)
|
|
||||||
.addConverterFactory(kotlinxConverterFactory)
|
|
||||||
.build()
|
|
||||||
.create<PipedApi>()
|
|
||||||
}
|
|
||||||
|
|
||||||
val externalApi by resettableLazy(lazyMgr) {
|
|
||||||
Retrofit.Builder()
|
|
||||||
.baseUrl(apiUrl)
|
|
||||||
.client(httpClient)
|
|
||||||
.addConverterFactory(kotlinxConverterFactory)
|
|
||||||
.build()
|
|
||||||
.create<ExternalApi>()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildClient(): OkHttpClient {
|
private fun buildClient(): OkHttpClient {
|
||||||
val httpClient = OkHttpClient().newBuilder()
|
val httpClient = OkHttpClient().newBuilder()
|
||||||
@ -74,4 +52,11 @@ object RetrofitInstance {
|
|||||||
|
|
||||||
return httpClient.build()
|
return httpClient.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T: Any> buildRetrofitInstance(apiUrl: String): T = Retrofit.Builder()
|
||||||
|
.baseUrl(apiUrl)
|
||||||
|
.client(httpClient)
|
||||||
|
.addConverterFactory(kotlinxConverterFactory)
|
||||||
|
.build()
|
||||||
|
.create<T>()
|
||||||
}
|
}
|
||||||
|
@ -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 RetrofitInstance.api.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
|
|
||||||
)
|
)
|
||||||
|
@ -133,6 +133,7 @@ object PreferenceKeys {
|
|||||||
const val MAX_CONCURRENT_DOWNLOADS = "max_parallel_downloads"
|
const val MAX_CONCURRENT_DOWNLOADS = "max_parallel_downloads"
|
||||||
const val EXTERNAL_DOWNLOAD_PROVIDER = "external_download_provider"
|
const val EXTERNAL_DOWNLOAD_PROVIDER = "external_download_provider"
|
||||||
const val DISABLE_VIDEO_IMAGE_PROXY = "disable_video_image_proxy"
|
const val DISABLE_VIDEO_IMAGE_PROXY = "disable_video_image_proxy"
|
||||||
|
const val FULL_LOCAL_MODE = "full_local_mode"
|
||||||
const val LOCAL_RYD = "local_return_youtube_dislikes"
|
const val LOCAL_RYD = "local_return_youtube_dislikes"
|
||||||
const val LOCAL_STREAM_EXTRACTION = "local_stream_extraction"
|
const val LOCAL_STREAM_EXTRACTION = "local_stream_extraction"
|
||||||
|
|
||||||
|
@ -357,6 +357,12 @@ object PlayerHelper {
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val fullLocalMode: Boolean
|
||||||
|
get() = PreferenceHelper.getBoolean(
|
||||||
|
PreferenceKeys.FULL_LOCAL_MODE,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
val localStreamExtraction: Boolean
|
val localStreamExtraction: Boolean
|
||||||
get() = PreferenceHelper.getBoolean(
|
get() = PreferenceHelper.getBoolean(
|
||||||
PreferenceKeys.LOCAL_STREAM_EXTRACTION,
|
PreferenceKeys.LOCAL_STREAM_EXTRACTION,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.github.libretube.helpers
|
package com.github.libretube.helpers
|
||||||
|
|
||||||
|
import com.github.libretube.api.PipedMediaServiceRepository
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.RetrofitInstance
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -11,9 +12,10 @@ object ProxyHelper {
|
|||||||
fun fetchProxyUrl() {
|
fun fetchProxyUrl() {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
RetrofitInstance.api.getConfig().imageProxyUrl?.let {
|
RetrofitInstance.externalApi.getInstanceConfig(PipedMediaServiceRepository.apiUrl)
|
||||||
PreferenceHelper.putString(PreferenceKeys.IMAGE_PROXY_URL, it)
|
.imageProxyUrl?.let {
|
||||||
}
|
PreferenceHelper.putString(PreferenceKeys.IMAGE_PROXY_URL, it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package com.github.libretube.repo
|
package com.github.libretube.repo
|
||||||
|
|
||||||
|
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.RetrofitInstance
|
|
||||||
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
|
||||||
@ -84,7 +83,7 @@ class LocalPlaylistsRepository: PlaylistRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun clonePlaylist(playlistId: String): String {
|
override suspend fun clonePlaylist(playlistId: String): String {
|
||||||
val playlist = RetrofitInstance.api.getPlaylist(playlistId)
|
val playlist = MediaServiceRepository.instance.getPlaylist(playlistId)
|
||||||
val newPlaylist = createPlaylist(playlist.name ?: "Unknown name")
|
val newPlaylist = createPlaylist(playlist.name ?: "Unknown name")
|
||||||
|
|
||||||
PlaylistsHelper.addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray())
|
PlaylistsHelper.addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray())
|
||||||
@ -92,7 +91,7 @@ class LocalPlaylistsRepository: PlaylistRepository {
|
|||||||
var nextPage = playlist.nextpage
|
var nextPage = playlist.nextpage
|
||||||
while (nextPage != null) {
|
while (nextPage != null) {
|
||||||
nextPage = runCatching {
|
nextPage = runCatching {
|
||||||
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage!!).apply {
|
MediaServiceRepository.instance.getPlaylistNextPage(playlistId, nextPage!!).apply {
|
||||||
PlaylistsHelper.addToPlaylist(newPlaylist, *relatedStreams.toTypedArray())
|
PlaylistsHelper.addToPlaylist(newPlaylist, *relatedStreams.toTypedArray())
|
||||||
}.nextpage
|
}.nextpage
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
@ -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
|
||||||
|
@ -12,15 +12,13 @@ import androidx.media3.datasource.DefaultDataSource
|
|||||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
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.RetrofitInstance
|
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
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.db.DatabaseHelper
|
import com.github.libretube.db.DatabaseHelper
|
||||||
import com.github.libretube.db.DatabaseHolder
|
|
||||||
import com.github.libretube.enums.PlayerCommand
|
import com.github.libretube.enums.PlayerCommand
|
||||||
import com.github.libretube.extensions.parcelable
|
import com.github.libretube.extensions.parcelable
|
||||||
import com.github.libretube.extensions.setMetadata
|
import com.github.libretube.extensions.setMetadata
|
||||||
@ -38,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.
|
||||||
@ -120,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
|
||||||
@ -206,7 +206,7 @@ open class OnlinePlayerService : AbstractPlayerService() {
|
|||||||
private fun fetchSponsorBlockSegments() = scope.launch(Dispatchers.IO) {
|
private fun fetchSponsorBlockSegments() = scope.launch(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (sponsorBlockConfig.isEmpty()) return@runCatching
|
if (sponsorBlockConfig.isEmpty()) return@runCatching
|
||||||
sponsorBlockSegments = RetrofitInstance.api.getSegments(
|
sponsorBlockSegments = MediaServiceRepository.instance.getSegments(
|
||||||
videoId,
|
videoId,
|
||||||
JsonHelper.json.encodeToString(sponsorBlockConfig.keys),
|
JsonHelper.json.encodeToString(sponsorBlockConfig.keys),
|
||||||
"""["skip","mute","full","poi","chapter"]"""
|
"""["skip","mute","full","poi","chapter"]"""
|
||||||
|
@ -10,9 +10,8 @@ import androidx.lifecycle.LifecycleService
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.libretube.LibreTubeApp.Companion.PLAYLIST_DOWNLOAD_ENQUEUE_CHANNEL_NAME
|
import com.github.libretube.LibreTubeApp.Companion.PLAYLIST_DOWNLOAD_ENQUEUE_CHANNEL_NAME
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
|
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.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
|
||||||
@ -95,7 +94,7 @@ class PlaylistDownloadEnqueueService : LifecycleService() {
|
|||||||
|
|
||||||
private suspend fun enqueuePublicPlaylist() {
|
private suspend fun enqueuePublicPlaylist() {
|
||||||
val playlist = try {
|
val playlist = try {
|
||||||
RetrofitInstance.api.getPlaylist(playlistId)
|
MediaServiceRepository.instance.getPlaylist(playlistId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
toastFromMainDispatcher(e.localizedMessage.orEmpty())
|
toastFromMainDispatcher(e.localizedMessage.orEmpty())
|
||||||
stopSelf()
|
stopSelf()
|
||||||
@ -111,7 +110,7 @@ class PlaylistDownloadEnqueueService : LifecycleService() {
|
|||||||
|
|
||||||
while (nextPage != null) {
|
while (nextPage != null) {
|
||||||
val playlistPage = runCatching {
|
val playlistPage = runCatching {
|
||||||
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage!!)
|
MediaServiceRepository.instance.getPlaylistNextPage(playlistId, nextPage!!)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
if (playlistPage == null && alreadyRetriedOnce) {
|
if (playlistPage == null && alreadyRetriedOnce) {
|
||||||
@ -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)
|
||||||
|
@ -6,6 +6,7 @@ import android.os.Bundle
|
|||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.RetrofitInstance
|
||||||
import com.github.libretube.api.obj.DeArrowBody
|
import com.github.libretube.api.obj.DeArrowBody
|
||||||
import com.github.libretube.api.obj.DeArrowSubmitThumbnail
|
import com.github.libretube.api.obj.DeArrowSubmitThumbnail
|
||||||
@ -71,7 +72,7 @@ class SubmitDeArrowDialog: DialogFragment() {
|
|||||||
private suspend fun fetchDeArrowData() {
|
private suspend fun fetchDeArrowData() {
|
||||||
val data = try {
|
val data = try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
RetrofitInstance.api.getDeArrowContent(videoId)
|
MediaServiceRepository.instance.getDeArrowContent(videoId)
|
||||||
}.getOrElse(videoId) { return }
|
}.getOrElse(videoId) { return }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return
|
return
|
||||||
|
@ -10,6 +10,7 @@ import androidx.fragment.app.DialogFragment
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
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.RetrofitInstance
|
import com.github.libretube.api.RetrofitInstance
|
||||||
import com.github.libretube.api.obj.Segment
|
import com.github.libretube.api.obj.Segment
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
@ -146,7 +147,7 @@ class SubmitSegmentDialog : DialogFragment() {
|
|||||||
private suspend fun fetchSegments() {
|
private suspend fun fetchSegments() {
|
||||||
val categories = resources.getStringArray(R.array.sponsorBlockSegments).toList()
|
val categories = resources.getStringArray(R.array.sponsorBlockSegments).toList()
|
||||||
segments = try {
|
segments = try {
|
||||||
RetrofitInstance.api.getSegments(videoId, JsonHelper.json.encodeToString(categories)).segments
|
MediaServiceRepository.instance.getSegments(videoId, JsonHelper.json.encodeToString(categories)).segments
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG(), e.toString())
|
Log.e(TAG(), e.toString())
|
||||||
return
|
return
|
||||||
|
@ -11,7 +11,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.api.obj.ChannelTab
|
import com.github.libretube.api.obj.ChannelTab
|
||||||
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
|
||||||
@ -47,7 +47,7 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
|
|||||||
|
|
||||||
private suspend fun fetchChannelNextPage(nextPage: String): String? {
|
private suspend fun fetchChannelNextPage(nextPage: String): String? {
|
||||||
val response = withContext(Dispatchers.IO) {
|
val response = withContext(Dispatchers.IO) {
|
||||||
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage).apply {
|
MediaServiceRepository.instance.getChannelNextPage(channelId!!, nextPage).apply {
|
||||||
relatedStreams = relatedStreams.deArrow()
|
relatedStreams = relatedStreams.deArrow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,7 +57,7 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
|
|||||||
|
|
||||||
private suspend fun fetchTabNextPage(nextPage: String, tab: ChannelTab): String? {
|
private suspend fun fetchTabNextPage(nextPage: String, tab: ChannelTab): String? {
|
||||||
val newContent = withContext(Dispatchers.IO) {
|
val newContent = withContext(Dispatchers.IO) {
|
||||||
RetrofitInstance.api.getChannelTab(tab.data, nextPage)
|
MediaServiceRepository.instance.getChannelTab(tab.data, nextPage)
|
||||||
}.apply {
|
}.apply {
|
||||||
content = content.deArrow()
|
content = content.deArrow()
|
||||||
}
|
}
|
||||||
@ -77,7 +77,7 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
|
|||||||
private fun loadChannelTab(tab: ChannelTab) = lifecycleScope.launch {
|
private fun loadChannelTab(tab: ChannelTab) = lifecycleScope.launch {
|
||||||
val response = try {
|
val response = try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
RetrofitInstance.api.getChannelTab(tab.data)
|
MediaServiceRepository.instance.getChannelTab(tab.data)
|
||||||
}.apply {
|
}.apply {
|
||||||
content = content.deArrow()
|
content = content.deArrow()
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.api.obj.ChannelTab
|
import com.github.libretube.api.obj.ChannelTab
|
||||||
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
|
||||||
@ -122,9 +122,9 @@ class ChannelFragment : DynamicLayoutManagerFragment(R.layout.fragment_channel)
|
|||||||
val response = try {
|
val response = try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (channelId != null) {
|
if (channelId != null) {
|
||||||
RetrofitInstance.api.getChannel(channelId!!)
|
MediaServiceRepository.instance.getChannel(channelId!!)
|
||||||
} else {
|
} else {
|
||||||
RetrofitInstance.api.getChannelByName(channelName!!)
|
MediaServiceRepository.instance.getChannelByName(channelName!!)
|
||||||
}.apply {
|
}.apply {
|
||||||
relatedStreams = relatedStreams.deArrow()
|
relatedStreams = relatedStreams.deArrow()
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,8 @@ import androidx.navigation.fragment.navArgs
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
|
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.obj.Playlist
|
import com.github.libretube.api.obj.Playlist
|
||||||
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
|
||||||
@ -382,7 +382,7 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist
|
|||||||
val response = try {
|
val response = try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// load locally stored playlists with the auth api
|
// load locally stored playlists with the auth api
|
||||||
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage!!)
|
MediaServiceRepository.instance.getPlaylistNextPage(playlistId, nextPage!!)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
context?.toastFromMainDispatcher(e.localizedMessage.orEmpty())
|
context?.toastFromMainDispatcher(e.localizedMessage.orEmpty())
|
||||||
|
@ -12,7 +12,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.databinding.FragmentSearchSuggestionsBinding
|
import com.github.libretube.databinding.FragmentSearchSuggestionsBinding
|
||||||
@ -102,7 +102,7 @@ class SearchSuggestionsFragment : Fragment(R.layout.fragment_search_suggestions)
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val response = try {
|
val response = try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
RetrofitInstance.api.getSuggestions(query)
|
MediaServiceRepository.instance.getSuggestions(query)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG(), e.toString())
|
Log.e(TAG(), e.toString())
|
||||||
|
@ -4,8 +4,8 @@ import android.content.Context
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
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.SubscriptionHelper
|
import com.github.libretube.api.SubscriptionHelper
|
||||||
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
|
||||||
@ -78,7 +78,7 @@ class HomeViewModel : ViewModel() {
|
|||||||
|
|
||||||
runSafely(
|
runSafely(
|
||||||
onSuccess = { videos -> trending.updateIfChanged(videos) },
|
onSuccess = { videos -> trending.updateIfChanged(videos) },
|
||||||
ioBlock = { RetrofitInstance.api.getTrending(region).deArrow().take(10) }
|
ioBlock = { MediaServiceRepository.instance.getTrending(region).deArrow().take(10) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.api.obj.StreamItem
|
import com.github.libretube.api.obj.StreamItem
|
||||||
import com.github.libretube.extensions.TAG
|
import com.github.libretube.extensions.TAG
|
||||||
import com.github.libretube.helpers.LocaleHelper
|
import com.github.libretube.helpers.LocaleHelper
|
||||||
@ -28,7 +28,7 @@ class TrendsViewModel : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
val region = LocaleHelper.getTrendingRegion(context)
|
val region = LocaleHelper.getTrendingRegion(context)
|
||||||
val response = withContext(Dispatchers.IO) {
|
val response = withContext(Dispatchers.IO) {
|
||||||
RetrofitInstance.api.getTrending(region).deArrow()
|
MediaServiceRepository.instance.getTrending(region).deArrow()
|
||||||
}
|
}
|
||||||
trendingVideos.postValue(response)
|
trendingVideos.postValue(response)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
@ -77,7 +77,7 @@ class WelcomeViewModel(
|
|||||||
|
|
||||||
private fun refreshAndNavigate() {
|
private fun refreshAndNavigate() {
|
||||||
// refresh the api urls since they have changed likely
|
// refresh the api urls since they have changed likely
|
||||||
RetrofitInstance.lazyMgr.reset()
|
RetrofitInstance.apiLazyMgr.reset()
|
||||||
savedStateHandle[UI_STATE] = _uiState.value.copy(navigateToMain = Unit)
|
savedStateHandle[UI_STATE] = _uiState.value.copy(navigateToMain = Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,10 @@ package com.github.libretube.ui.models.sources
|
|||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import com.github.libretube.api.RetrofitInstance
|
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) {
|
||||||
RetrofitInstance.api.getCommentsNextPage(videoId, it)
|
params.key?.let {
|
||||||
} ?: RetrofitInstance.api.getComments(videoId)
|
MediaServiceRepository.instance.getCommentsNextPage(videoId, it)
|
||||||
|
} ?: MediaServiceRepository.instance.getComments(videoId)
|
||||||
|
}
|
||||||
|
|
||||||
if (result.commentCount > 0) onCommentCount(result.commentCount)
|
if (result.commentCount > 0) onCommentCount(result.commentCount)
|
||||||
|
|
||||||
|
@ -2,8 +2,10 @@ package com.github.libretube.ui.models.sources
|
|||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import com.github.libretube.api.RetrofitInstance
|
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 = RetrofitInstance.api.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()) {
|
||||||
|
@ -2,9 +2,11 @@ package com.github.libretube.ui.models.sources
|
|||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import com.github.libretube.api.RetrofitInstance
|
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) {
|
||||||
RetrofitInstance.api.getSearchResultsNextPage(searchQuery, searchFilter, it)
|
params.key?.let {
|
||||||
} ?: RetrofitInstance.api.getSearchResults(searchQuery, searchFilter)
|
MediaServiceRepository.instance.getSearchResultsNextPage(
|
||||||
|
searchQuery, searchFilter, it
|
||||||
|
)
|
||||||
|
} ?: 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)
|
||||||
|
@ -11,6 +11,7 @@ import androidx.preference.SwitchPreferenceCompat
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.InstanceRepository
|
import com.github.libretube.api.InstanceRepository
|
||||||
|
import com.github.libretube.api.PipedMediaServiceRepository
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.RetrofitInstance
|
||||||
import com.github.libretube.api.obj.PipedInstance
|
import com.github.libretube.api.obj.PipedInstance
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
@ -68,13 +69,13 @@ class InstanceSettings : BasePreferenceFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authInstance.setOnPreferenceChangeListener { _, _ ->
|
authInstance.setOnPreferenceChangeListener { _, _ ->
|
||||||
RetrofitInstance.lazyMgr.reset()
|
RetrofitInstance.apiLazyMgr.reset()
|
||||||
logoutAndUpdateUI()
|
logoutAndUpdateUI()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
authInstanceToggle.setOnPreferenceChangeListener { _, _ ->
|
authInstanceToggle.setOnPreferenceChangeListener { _, _ ->
|
||||||
RetrofitInstance.lazyMgr.reset()
|
RetrofitInstance.apiLazyMgr.reset()
|
||||||
logoutAndUpdateUI()
|
logoutAndUpdateUI()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -147,7 +148,7 @@ class InstanceSettings : BasePreferenceFragment() {
|
|||||||
|
|
||||||
// add the currently used instances to the list if they're currently down / not part
|
// add the currently used instances to the list if they're currently down / not part
|
||||||
// of the public instances list
|
// of the public instances list
|
||||||
for (apiUrl in listOf(RetrofitInstance.apiUrl, RetrofitInstance.authUrl)) {
|
for (apiUrl in listOf(PipedMediaServiceRepository.apiUrl, RetrofitInstance.authUrl)) {
|
||||||
if (instances.none { it.apiUrl == apiUrl }) {
|
if (instances.none { it.apiUrl == apiUrl }) {
|
||||||
val origin = apiUrl.toHttpUrl().host
|
val origin = apiUrl.toHttpUrl().host
|
||||||
instances.add(PipedInstance(origin, apiUrl, isCurrentlyDown = true))
|
instances.add(PipedInstance(origin, apiUrl, isCurrentlyDown = true))
|
||||||
@ -215,7 +216,7 @@ class InstanceSettings : BasePreferenceFragment() {
|
|||||||
if (!authInstanceToggle.isChecked) {
|
if (!authInstanceToggle.isChecked) {
|
||||||
logoutAndUpdateUI()
|
logoutAndUpdateUI()
|
||||||
}
|
}
|
||||||
RetrofitInstance.lazyMgr.reset()
|
RetrofitInstance.apiLazyMgr.reset()
|
||||||
ActivityCompat.recreate(requireActivity())
|
ActivityCompat.recreate(requireActivity())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.MediaServiceRepository
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.enums.ShareObjectType
|
import com.github.libretube.enums.ShareObjectType
|
||||||
import com.github.libretube.extensions.TAG
|
import com.github.libretube.extensions.TAG
|
||||||
@ -66,7 +66,7 @@ class ChannelOptionsBottomSheet : BaseBottomSheet() {
|
|||||||
R.string.play_latest_videos -> {
|
R.string.play_latest_videos -> {
|
||||||
try {
|
try {
|
||||||
val channel = withContext(Dispatchers.IO) {
|
val channel = withContext(Dispatchers.IO) {
|
||||||
RetrofitInstance.api.getChannel(channelId)
|
MediaServiceRepository.instance.getChannel(channelId)
|
||||||
}
|
}
|
||||||
channel.relatedStreams.firstOrNull()?.url?.toID()?.let {
|
channel.relatedStreams.firstOrNull()?.url?.toID()?.let {
|
||||||
NavigationHelper.navigateVideo(
|
NavigationHelper.navigateVideo(
|
||||||
@ -83,7 +83,7 @@ class ChannelOptionsBottomSheet : BaseBottomSheet() {
|
|||||||
R.string.playOnBackground -> {
|
R.string.playOnBackground -> {
|
||||||
try {
|
try {
|
||||||
val channel = withContext(Dispatchers.IO) {
|
val channel = withContext(Dispatchers.IO) {
|
||||||
RetrofitInstance.api.getChannel(channelId)
|
MediaServiceRepository.instance.getChannel(channelId)
|
||||||
}
|
}
|
||||||
channel.relatedStreams.firstOrNull()?.url?.toID()?.let {
|
channel.relatedStreams.firstOrNull()?.url?.toID()?.let {
|
||||||
BackgroundHelper.playOnBackground(
|
BackgroundHelper.playOnBackground(
|
||||||
|
@ -3,8 +3,8 @@ package com.github.libretube.ui.sheets
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
|
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.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.db.DatabaseHolder
|
import com.github.libretube.db.DatabaseHolder
|
||||||
import com.github.libretube.enums.ImportFormat
|
import com.github.libretube.enums.ImportFormat
|
||||||
@ -176,7 +176,7 @@ class PlaylistOptionsBottomSheet : BaseBottomSheet() {
|
|||||||
DatabaseHolder.Database.playlistBookmarkDao().deleteById(playlistId)
|
DatabaseHolder.Database.playlistBookmarkDao().deleteById(playlistId)
|
||||||
} else {
|
} else {
|
||||||
val bookmark = try {
|
val bookmark = try {
|
||||||
RetrofitInstance.api.getPlaylist(playlistId)
|
MediaServiceRepository.instance.getPlaylist(playlistId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return@withContext
|
return@withContext
|
||||||
}.toPlaylistBookmark(playlistId)
|
}.toPlaylistBookmark(playlistId)
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package com.github.libretube.util
|
package com.github.libretube.util
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.github.libretube.api.RetrofitInstance
|
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
|
||||||
@ -26,7 +27,7 @@ object DeArrowUtil {
|
|||||||
val videoIdsString = videoIds.mapTo(TreeSet()) { it }.joinToString(",")
|
val videoIdsString = videoIds.mapTo(TreeSet()) { it }.joinToString(",")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
RetrofitInstance.api.getDeArrowContent(videoIdsString)
|
MediaServiceRepository.instance.getDeArrowContent(videoIdsString)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(this::class.java.name, "Failed to fetch DeArrow content: ${e.message}")
|
Log.e(this::class.java.name, "Failed to fetch DeArrow content: ${e.message}")
|
||||||
null
|
null
|
||||||
@ -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
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package com.github.libretube.util
|
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.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
|
||||||
@ -158,7 +157,7 @@ object PlayingQueue {
|
|||||||
) {
|
) {
|
||||||
var playlistNextPage = nextPage
|
var playlistNextPage = nextPage
|
||||||
while (playlistNextPage != null) {
|
while (playlistNextPage != null) {
|
||||||
RetrofitInstance.api.getPlaylistNextPage(playlistId, playlistNextPage).run {
|
MediaServiceRepository.instance.getPlaylistNextPage(playlistId, playlistNextPage).run {
|
||||||
addToQueueAsync(relatedStreams, isMainList = isMainList)
|
addToQueueAsync(relatedStreams, isMainList = isMainList)
|
||||||
playlistNextPage = this.nextpage
|
playlistNextPage = this.nextpage
|
||||||
}
|
}
|
||||||
@ -177,7 +176,7 @@ object PlayingQueue {
|
|||||||
var channelNextPage = nextPage
|
var channelNextPage = nextPage
|
||||||
var pageIndex = 1
|
var pageIndex = 1
|
||||||
while (channelNextPage != null && pageIndex < 10) {
|
while (channelNextPage != null && pageIndex < 10) {
|
||||||
RetrofitInstance.api.getChannelNextPage(channelId, channelNextPage).run {
|
MediaServiceRepository.instance.getChannelNextPage(channelId, channelNextPage).run {
|
||||||
addToQueueAsync(relatedStreams)
|
addToQueueAsync(relatedStreams)
|
||||||
channelNextPage = this.nextpage
|
channelNextPage = this.nextpage
|
||||||
pageIndex++
|
pageIndex++
|
||||||
@ -186,14 +185,14 @@ object PlayingQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun insertChannel(channelId: String, newCurrentStream: StreamItem) = runCatchingIO {
|
private fun insertChannel(channelId: String, newCurrentStream: StreamItem) = runCatchingIO {
|
||||||
val channel = RetrofitInstance.api.getChannel(channelId)
|
val channel = MediaServiceRepository.instance.getChannel(channelId)
|
||||||
addToQueueAsync(channel.relatedStreams, newCurrentStream)
|
addToQueueAsync(channel.relatedStreams, newCurrentStream)
|
||||||
if (channel.nextpage == null) return@runCatchingIO
|
if (channel.nextpage == null) return@runCatchingIO
|
||||||
fetchMoreFromChannel(channelId, channel.nextpage)
|
fetchMoreFromChannel(channelId, channel.nextpage)
|
||||||
}.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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -472,6 +472,7 @@
|
|||||||
<string name="crashlog">Crashlog</string>
|
<string name="crashlog">Crashlog</string>
|
||||||
<string name="never_show_again">Never show this again</string>
|
<string name="never_show_again">Never show this again</string>
|
||||||
<string name="update_information">Update information</string>
|
<string name="update_information">Update information</string>
|
||||||
|
<string name="mode_of_operation">Mode of operation</string>
|
||||||
|
|
||||||
<!-- Backup & Restore Settings -->
|
<!-- Backup & Restore Settings -->
|
||||||
<string name="import_subscriptions_from">Import subscriptions from</string>
|
<string name="import_subscriptions_from">Import subscriptions from</string>
|
||||||
@ -532,6 +533,9 @@
|
|||||||
<string name="local_feed_extraction_summary">Directly fetch the feed from YouTube. This may be significantly slower.</string>
|
<string name="local_feed_extraction_summary">Directly fetch the feed from YouTube. This may be significantly slower.</string>
|
||||||
<string name="show_upcoming_videos">Show upcoming videos</string>
|
<string name="show_upcoming_videos">Show upcoming videos</string>
|
||||||
<string name="updating_feed">Updating feed …</string>
|
<string name="updating_feed">Updating feed …</string>
|
||||||
|
<string name="full_local_mode">Full local mode</string>
|
||||||
|
<string name="full_local_mode_desc">Directly fetch everything from YouTube, without using Piped.</string>
|
||||||
|
<string name="authentication">Authentication</string>
|
||||||
|
|
||||||
<!-- Notification channel strings -->
|
<!-- Notification channel strings -->
|
||||||
<string name="download_channel_name">Download Service</string>
|
<string name="download_channel_name">Download Service</string>
|
||||||
|
@ -2,6 +2,26 @@
|
|||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<PreferenceCategory app:title="@string/mode_of_operation">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:icon="@drawable/ic_region"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:title="@string/full_local_mode"
|
||||||
|
android:key="full_local_mode"
|
||||||
|
android:disableDependentsState="true"
|
||||||
|
android:summary="@string/full_local_mode_desc" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="true"
|
||||||
|
android:icon="@drawable/ic_region"
|
||||||
|
android:dependency="full_local_mode"
|
||||||
|
android:summary="@string/local_stream_extraction_summary"
|
||||||
|
android:title="@string/local_stream_extraction"
|
||||||
|
app:key="local_stream_extraction" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/instance">
|
<PreferenceCategory app:title="@string/instance">
|
||||||
|
|
||||||
<ListPreference
|
<ListPreference
|
||||||
@ -10,19 +30,41 @@
|
|||||||
app:entries="@array/instances"
|
app:entries="@array/instances"
|
||||||
app:entryValues="@array/instancesValue"
|
app:entryValues="@array/instancesValue"
|
||||||
app:key="selectInstance"
|
app:key="selectInstance"
|
||||||
|
android:dependency="full_local_mode"
|
||||||
app:title="@string/instances" />
|
app:title="@string/instances" />
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_add_instance"
|
android:icon="@drawable/ic_add_instance"
|
||||||
app:key="customInstance"
|
app:key="customInstance"
|
||||||
|
android:dependency="full_local_mode"
|
||||||
app:summary="@string/customInstance_summary"
|
app:summary="@string/customInstance_summary"
|
||||||
app:title="@string/customInstance" />
|
app:title="@string/customInstance" />
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_trash"
|
android:icon="@drawable/ic_trash"
|
||||||
app:key="clearCustomInstances"
|
app:key="clearCustomInstances"
|
||||||
|
android:dependency="full_local_mode"
|
||||||
app:title="@string/clear_customInstances" />
|
app:title="@string/clear_customInstances" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:icon="@drawable/ic_server"
|
||||||
|
android:dependency="full_local_mode"
|
||||||
|
android:summary="@string/disable_proxy_summary"
|
||||||
|
android:title="@string/disable_proxy"
|
||||||
|
app:key="disable_video_image_proxy" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<PreferenceCategory android:title="@string/authentication">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:icon="@drawable/ic_region"
|
||||||
|
android:summary="@string/local_feed_extraction_summary"
|
||||||
|
android:title="@string/local_feed_extraction"
|
||||||
|
app:key="local_feed_extraction" />
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:icon="@drawable/ic_auth"
|
android:icon="@drawable/ic_auth"
|
||||||
@ -39,21 +81,6 @@
|
|||||||
app:key="selectAuthInstance"
|
app:key="selectAuthInstance"
|
||||||
app:title="@string/auth_instances" />
|
app:title="@string/auth_instances" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/audio_video">
|
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:icon="@drawable/ic_list"
|
|
||||||
android:summary="@string/hls_instead_of_dash_summary"
|
|
||||||
android:title="@string/hls_instead_of_dash"
|
|
||||||
app:key="use_hls" />
|
|
||||||
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/account">
|
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_login_filled"
|
android:icon="@drawable/ic_login_filled"
|
||||||
android:summary="@string/notgmail"
|
android:summary="@string/notgmail"
|
||||||
@ -75,42 +102,27 @@
|
|||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/proxy">
|
<PreferenceCategory app:title="@string/audio_video">
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:icon="@drawable/ic_server"
|
android:icon="@drawable/ic_list"
|
||||||
android:summary="@string/disable_proxy_summary"
|
android:summary="@string/hls_instead_of_dash_summary"
|
||||||
android:title="@string/disable_proxy"
|
android:title="@string/hls_instead_of_dash"
|
||||||
app:key="disable_video_image_proxy" />
|
app:key="use_hls" />
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
</PreferenceCategory>
|
||||||
android:defaultValue="true"
|
|
||||||
android:icon="@drawable/ic_region"
|
<PreferenceCategory app:title="@string/misc">
|
||||||
android:summary="@string/local_stream_extraction_summary"
|
|
||||||
android:title="@string/local_stream_extraction"
|
|
||||||
android:dependency="disable_video_image_proxy"
|
|
||||||
app:key="local_stream_extraction" />
|
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
android:icon="@drawable/ic_dislike"
|
android:icon="@drawable/ic_dislike"
|
||||||
|
android:dependency="local_stream_extraction"
|
||||||
android:summary="@string/local_ryd_summary"
|
android:summary="@string/local_ryd_summary"
|
||||||
android:title="@string/local_ryd"
|
android:title="@string/local_ryd"
|
||||||
android:dependency="local_stream_extraction"
|
|
||||||
app:key="local_return_youtube_dislikes" />
|
app:key="local_return_youtube_dislikes" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/subscriptions">
|
|
||||||
|
|
||||||
<SwitchPreferenceCompat
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:icon="@drawable/ic_region"
|
|
||||||
android:summary="@string/local_feed_extraction_summary"
|
|
||||||
android:title="@string/local_feed_extraction"
|
|
||||||
app:key="local_feed_extraction" />
|
|
||||||
|
|
||||||
</PreferenceCategory>
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
Loading…
x
Reference in New Issue
Block a user