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
|
||||
|
||||
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.SubmitSegmentResponse
|
||||
import com.github.libretube.api.obj.VoteInfo
|
||||
@ -20,6 +21,9 @@ interface ExternalApi {
|
||||
@GET
|
||||
suspend fun getInstances(@Url url: String): List<PipedInstance>
|
||||
|
||||
@GET("config")
|
||||
suspend fun getInstanceConfig(@Url url: String): PipedConfig
|
||||
|
||||
// fetch latest version info
|
||||
@GET(GITHUB_API_URL)
|
||||
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.CommentsPage
|
||||
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.Playlists
|
||||
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.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 PipedApi {
|
||||
@GET("config")
|
||||
suspend fun getConfig(): PipedConfig
|
||||
|
||||
@GET("trending")
|
||||
suspend fun getTrending(@Query("region") region: String): List<StreamItem>
|
||||
|
||||
@ -98,106 +81,4 @@ interface PipedApi {
|
||||
@Path("playlistId") playlistId: String,
|
||||
@Query("nextpage") nextPage: String
|
||||
): 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 {
|
||||
// load locally stored playlists with the auth api
|
||||
return when (getPrivatePlaylistType(playlistId)) {
|
||||
PlaylistType.PUBLIC -> RetrofitInstance.api.getPlaylist(playlistId)
|
||||
PlaylistType.PUBLIC -> MediaServiceRepository.instance.getPlaylist(playlistId)
|
||||
else -> playlistsRepository.getPlaylist(playlistId)
|
||||
}.apply {
|
||||
relatedStreams = relatedStreams.deArrow()
|
||||
|
@ -11,55 +11,33 @@ import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import retrofit2.create
|
||||
|
||||
object RetrofitInstance {
|
||||
private const val PIPED_API_URL = "https://pipedapi.kavin.rocks"
|
||||
val apiUrl get() = PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, PIPED_API_URL)
|
||||
const val PIPED_API_URL = "https://pipedapi.kavin.rocks"
|
||||
|
||||
val authUrl
|
||||
get() = when (
|
||||
get() = if (
|
||||
PreferenceHelper.getBoolean(
|
||||
PreferenceKeys.AUTH_INSTANCE_TOGGLE,
|
||||
false
|
||||
)
|
||||
) {
|
||||
true -> PreferenceHelper.getString(
|
||||
PreferenceHelper.getString(
|
||||
PreferenceKeys.AUTH_INSTANCE,
|
||||
PIPED_API_URL
|
||||
)
|
||||
|
||||
false -> apiUrl
|
||||
} else {
|
||||
PipedMediaServiceRepository.apiUrl
|
||||
}
|
||||
|
||||
val lazyMgr = resettableManager()
|
||||
private val kotlinxConverterFactory = JsonHelper.json
|
||||
val apiLazyMgr = resettableManager()
|
||||
val kotlinxConverterFactory = JsonHelper.json
|
||||
.asConverterFactory("application/json".toMediaType())
|
||||
|
||||
private val httpClient by lazy { buildClient() }
|
||||
val httpClient by lazy { buildClient() }
|
||||
|
||||
val api by resettableLazy(lazyMgr) {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(apiUrl)
|
||||
.client(httpClient)
|
||||
.addConverterFactory(kotlinxConverterFactory)
|
||||
.build()
|
||||
.create<PipedApi>()
|
||||
}
|
||||
val authApi = buildRetrofitInstance<PipedAuthApi>(authUrl)
|
||||
|
||||
val authApi by resettableLazy(lazyMgr) {
|
||||
Retrofit.Builder()
|
||||
.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>()
|
||||
}
|
||||
// the url provided here isn't actually used anywhere in the external api
|
||||
val externalApi = buildRetrofitInstance<ExternalApi>(PIPED_API_URL)
|
||||
|
||||
private fun buildClient(): OkHttpClient {
|
||||
val httpClient = OkHttpClient().newBuilder()
|
||||
@ -74,4 +52,11 @@ object RetrofitInstance {
|
||||
|
||||
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 bannerUrl: String? = null,
|
||||
val description: String? = null,
|
||||
val nextpage: String? = null,
|
||||
var nextpage: String? = null,
|
||||
val subscriberCount: Long = 0,
|
||||
val verified: Boolean = false,
|
||||
var relatedStreams: List<StreamItem> = emptyList(),
|
||||
|
@ -6,6 +6,4 @@ import kotlinx.serialization.Serializable
|
||||
data class SearchResult(
|
||||
var items: List<ContentItem> = emptyList(),
|
||||
val nextpage: String? = null,
|
||||
val suggestion: String? = null,
|
||||
val corrected: Boolean? = null
|
||||
)
|
||||
|
@ -133,6 +133,7 @@ object PreferenceKeys {
|
||||
const val MAX_CONCURRENT_DOWNLOADS = "max_parallel_downloads"
|
||||
const val EXTERNAL_DOWNLOAD_PROVIDER = "external_download_provider"
|
||||
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_STREAM_EXTRACTION = "local_stream_extraction"
|
||||
|
||||
|
@ -357,6 +357,12 @@ object PlayerHelper {
|
||||
false
|
||||
)
|
||||
|
||||
val fullLocalMode: Boolean
|
||||
get() = PreferenceHelper.getBoolean(
|
||||
PreferenceKeys.FULL_LOCAL_MODE,
|
||||
false
|
||||
)
|
||||
|
||||
val localStreamExtraction: Boolean
|
||||
get() = PreferenceHelper.getBoolean(
|
||||
PreferenceKeys.LOCAL_STREAM_EXTRACTION,
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.github.libretube.helpers
|
||||
|
||||
import com.github.libretube.api.PipedMediaServiceRepository
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -11,9 +12,10 @@ object ProxyHelper {
|
||||
fun fetchProxyUrl() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
runCatching {
|
||||
RetrofitInstance.api.getConfig().imageProxyUrl?.let {
|
||||
PreferenceHelper.putString(PreferenceKeys.IMAGE_PROXY_URL, it)
|
||||
}
|
||||
RetrofitInstance.externalApi.getInstanceConfig(PipedMediaServiceRepository.apiUrl)
|
||||
.imageProxyUrl?.let {
|
||||
PreferenceHelper.putString(PreferenceKeys.IMAGE_PROXY_URL, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
package com.github.libretube.repo
|
||||
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.PlaylistsHelper
|
||||
import com.github.libretube.api.PlaylistsHelper.MAX_CONCURRENT_IMPORT_CALLS
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.obj.Playlist
|
||||
import com.github.libretube.api.obj.Playlists
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
@ -84,7 +83,7 @@ class LocalPlaylistsRepository: PlaylistRepository {
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
PlaylistsHelper.addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray())
|
||||
@ -92,7 +91,7 @@ class LocalPlaylistsRepository: PlaylistRepository {
|
||||
var nextPage = playlist.nextpage
|
||||
while (nextPage != null) {
|
||||
nextPage = runCatching {
|
||||
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage!!).apply {
|
||||
MediaServiceRepository.instance.getPlaylistNextPage(playlistId, nextPage!!).apply {
|
||||
PlaylistsHelper.addToPlaylist(newPlaylist, *relatedStreams.toTypedArray())
|
||||
}.nextpage
|
||||
}.getOrNull()
|
||||
@ -125,7 +124,7 @@ class LocalPlaylistsRepository: PlaylistRepository {
|
||||
// Only do so with `MAX_CONCURRENT_IMPORT_CALLS` videos at once to prevent performance issues
|
||||
for (videoIdList in playlist.videos.chunked(MAX_CONCURRENT_IMPORT_CALLS)) {
|
||||
val streams = videoIdList.parallelMap {
|
||||
runCatching { StreamsExtractor.extractStreams(it) }
|
||||
runCatching { MediaServiceRepository.instance.getStreams(it) }
|
||||
.getOrNull()
|
||||
?.toStreamItem(it)
|
||||
}.filterNotNull()
|
||||
|
@ -20,7 +20,7 @@ import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.LibreTubeApp.Companion.DOWNLOAD_CHANNEL_NAME
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.db.DatabaseHolder.Database
|
||||
@ -124,12 +124,13 @@ class DownloadService : LifecycleService() {
|
||||
lifecycleScope.launch(coroutineContext) {
|
||||
val streams = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
StreamsExtractor.extractStreams(videoId)
|
||||
MediaServiceRepository.instance.getStreams(videoId)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
toastFromMainDispatcher(getString(R.string.unknown_error))
|
||||
return@launch
|
||||
} catch (e: Exception) {
|
||||
toastFromMainDispatcher(
|
||||
StreamsExtractor.getExtractorErrorMessageString(this@DownloadService, e)
|
||||
)
|
||||
toastFromMainDispatcher(e.message ?: getString(R.string.server_error))
|
||||
return@launch
|
||||
}
|
||||
|
||||
@ -443,7 +444,7 @@ class DownloadService : LifecycleService() {
|
||||
*/
|
||||
private suspend fun regenerateLink(item: DownloadItem) {
|
||||
val streams = runCatching {
|
||||
StreamsExtractor.extractStreams(item.videoId)
|
||||
MediaServiceRepository.instance.getStreams(item.videoId)
|
||||
}.getOrNull() ?: return
|
||||
val stream = when (item.type) {
|
||||
FileType.AUDIO -> streams.audioStreams
|
||||
|
@ -12,15 +12,13 @@ import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.JsonHelper
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.SubscriptionHelper
|
||||
import com.github.libretube.api.obj.Segment
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.db.DatabaseHelper
|
||||
import com.github.libretube.db.DatabaseHolder
|
||||
import com.github.libretube.enums.PlayerCommand
|
||||
import com.github.libretube.extensions.parcelable
|
||||
import com.github.libretube.extensions.setMetadata
|
||||
@ -38,6 +36,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Loads the selected videos audio in background mode with a notification area.
|
||||
@ -120,11 +119,12 @@ open class OnlinePlayerService : AbstractPlayerService() {
|
||||
|
||||
streams = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
StreamsExtractor.extractStreams(videoId)
|
||||
MediaServiceRepository.instance.getStreams(videoId)
|
||||
} catch (e: IOException) {
|
||||
toastFromMainDispatcher(getString(R.string.unknown_error))
|
||||
return@withContext null
|
||||
} catch (e: Exception) {
|
||||
val errorMessage =
|
||||
StreamsExtractor.getExtractorErrorMessageString(this@OnlinePlayerService, e)
|
||||
this@OnlinePlayerService.toastFromMainDispatcher(errorMessage)
|
||||
toastFromMainDispatcher(e.message ?: getString(R.string.server_error))
|
||||
return@withContext null
|
||||
}
|
||||
} ?: return
|
||||
@ -206,7 +206,7 @@ open class OnlinePlayerService : AbstractPlayerService() {
|
||||
private fun fetchSponsorBlockSegments() = scope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
if (sponsorBlockConfig.isEmpty()) return@runCatching
|
||||
sponsorBlockSegments = RetrofitInstance.api.getSegments(
|
||||
sponsorBlockSegments = MediaServiceRepository.instance.getSegments(
|
||||
videoId,
|
||||
JsonHelper.json.encodeToString(sponsorBlockConfig.keys),
|
||||
"""["skip","mute","full","poi","chapter"]"""
|
||||
|
@ -10,9 +10,8 @@ import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.LibreTubeApp.Companion.PLAYLIST_DOWNLOAD_ENQUEUE_CHANNEL_NAME
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.PlaylistsHelper
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.obj.PipedStream
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.constants.IntentData
|
||||
@ -95,7 +94,7 @@ class PlaylistDownloadEnqueueService : LifecycleService() {
|
||||
|
||||
private suspend fun enqueuePublicPlaylist() {
|
||||
val playlist = try {
|
||||
RetrofitInstance.api.getPlaylist(playlistId)
|
||||
MediaServiceRepository.instance.getPlaylist(playlistId)
|
||||
} catch (e: Exception) {
|
||||
toastFromMainDispatcher(e.localizedMessage.orEmpty())
|
||||
stopSelf()
|
||||
@ -111,7 +110,7 @@ class PlaylistDownloadEnqueueService : LifecycleService() {
|
||||
|
||||
while (nextPage != null) {
|
||||
val playlistPage = runCatching {
|
||||
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage!!)
|
||||
MediaServiceRepository.instance.getPlaylistNextPage(playlistId, nextPage!!)
|
||||
}.getOrNull()
|
||||
|
||||
if (playlistPage == null && alreadyRetriedOnce) {
|
||||
@ -137,7 +136,7 @@ class PlaylistDownloadEnqueueService : LifecycleService() {
|
||||
|
||||
for (stream in streams) {
|
||||
val videoInfo = runCatching {
|
||||
StreamsExtractor.extractStreams(stream.url!!.toID())
|
||||
MediaServiceRepository.instance.getStreams(stream.url!!.toID())
|
||||
}.getOrNull() ?: continue
|
||||
|
||||
val videoStream = getStream(videoInfo.videoStreams, maxVideoQuality)
|
||||
|
@ -6,7 +6,7 @@ import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
||||
@ -41,7 +41,7 @@ class AddToPlaylistActivity : BaseActivity() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val videoInfo = if (PreferenceHelper.getToken().isEmpty()) {
|
||||
try {
|
||||
StreamsExtractor.extractStreams(videoId).toStreamItem(videoId)
|
||||
MediaServiceRepository.instance.getStreams(videoId).toStreamItem(videoId)
|
||||
} catch (e: Exception) {
|
||||
toastFromMainDispatcher(R.string.unknown_error)
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -159,12 +159,10 @@ class SearchChannelAdapter : ListAdapter<ContentItem, SearchViewHolder>(
|
||||
}
|
||||
|
||||
root.setOnLongClickListener {
|
||||
val playlistId = item.url.toID()
|
||||
val playlistName = item.name!!
|
||||
val sheet = PlaylistOptionsBottomSheet()
|
||||
sheet.arguments = bundleOf(
|
||||
IntentData.playlistId to playlistId,
|
||||
IntentData.playlistName to playlistName,
|
||||
IntentData.playlistId to item.url.toID(),
|
||||
IntentData.playlistName to item.name.orEmpty(),
|
||||
IntentData.playlistType to PlaylistType.PUBLIC
|
||||
)
|
||||
sheet.show(
|
||||
|
@ -5,7 +5,6 @@ import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isGone
|
||||
@ -13,13 +12,12 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.PipedStream
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.api.obj.Subtitle
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.databinding.DialogDownloadBinding
|
||||
import com.github.libretube.extensions.TAG
|
||||
import com.github.libretube.extensions.getWhileDigit
|
||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
||||
import com.github.libretube.helpers.DownloadHelper
|
||||
@ -30,6 +28,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
|
||||
class DownloadDialog : DialogFragment() {
|
||||
private lateinit var videoId: String
|
||||
@ -80,13 +79,13 @@ class DownloadDialog : DialogFragment() {
|
||||
lifecycleScope.launch {
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
StreamsExtractor.extractStreams(videoId)
|
||||
MediaServiceRepository.instance.getStreams(videoId)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
context?.toastFromMainDispatcher(getString(R.string.unknown_error))
|
||||
return@launch
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG(), e.stackTraceToString())
|
||||
val context = context ?: return@launch
|
||||
val errorMessage = StreamsExtractor.getExtractorErrorMessageString(context, e)
|
||||
context.toastFromMainDispatcher(errorMessage)
|
||||
context?.toastFromMainDispatcher(e.message ?: getString(R.string.server_error))
|
||||
return@launch
|
||||
}
|
||||
initDownloadOptions(binding, response)
|
||||
|
@ -6,6 +6,7 @@ import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.obj.DeArrowBody
|
||||
import com.github.libretube.api.obj.DeArrowSubmitThumbnail
|
||||
@ -71,7 +72,7 @@ class SubmitDeArrowDialog: DialogFragment() {
|
||||
private suspend fun fetchDeArrowData() {
|
||||
val data = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
RetrofitInstance.api.getDeArrowContent(videoId)
|
||||
MediaServiceRepository.instance.getDeArrowContent(videoId)
|
||||
}.getOrElse(videoId) { return }
|
||||
} catch (e: Exception) {
|
||||
return
|
||||
|
@ -10,6 +10,7 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.JsonHelper
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.obj.Segment
|
||||
import com.github.libretube.constants.IntentData
|
||||
@ -146,7 +147,7 @@ class SubmitSegmentDialog : DialogFragment() {
|
||||
private suspend fun fetchSegments() {
|
||||
val categories = resources.getStringArray(R.array.sponsorBlockSegments).toList()
|
||||
segments = try {
|
||||
RetrofitInstance.api.getSegments(videoId, JsonHelper.json.encodeToString(categories)).segments
|
||||
MediaServiceRepository.instance.getSegments(videoId, JsonHelper.json.encodeToString(categories)).segments
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG(), e.toString())
|
||||
return
|
||||
|
@ -11,7 +11,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.StreamItem
|
||||
import com.github.libretube.constants.IntentData
|
||||
@ -47,7 +47,7 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
|
||||
|
||||
private suspend fun fetchChannelNextPage(nextPage: String): String? {
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage).apply {
|
||||
MediaServiceRepository.instance.getChannelNextPage(channelId!!, nextPage).apply {
|
||||
relatedStreams = relatedStreams.deArrow()
|
||||
}
|
||||
}
|
||||
@ -57,7 +57,7 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
|
||||
|
||||
private suspend fun fetchTabNextPage(nextPage: String, tab: ChannelTab): String? {
|
||||
val newContent = withContext(Dispatchers.IO) {
|
||||
RetrofitInstance.api.getChannelTab(tab.data, nextPage)
|
||||
MediaServiceRepository.instance.getChannelTab(tab.data, nextPage)
|
||||
}.apply {
|
||||
content = content.deArrow()
|
||||
}
|
||||
@ -77,7 +77,7 @@ class ChannelContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_ch
|
||||
private fun loadChannelTab(tab: ChannelTab) = lifecycleScope.launch {
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
RetrofitInstance.api.getChannelTab(tab.data)
|
||||
MediaServiceRepository.instance.getChannelTab(tab.data)
|
||||
}.apply {
|
||||
content = content.deArrow()
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
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.StreamItem
|
||||
import com.github.libretube.constants.IntentData
|
||||
@ -122,9 +122,9 @@ class ChannelFragment : DynamicLayoutManagerFragment(R.layout.fragment_channel)
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (channelId != null) {
|
||||
RetrofitInstance.api.getChannel(channelId!!)
|
||||
MediaServiceRepository.instance.getChannel(channelId!!)
|
||||
} else {
|
||||
RetrofitInstance.api.getChannelByName(channelName!!)
|
||||
MediaServiceRepository.instance.getChannelByName(channelName!!)
|
||||
}.apply {
|
||||
relatedStreams = relatedStreams.deArrow()
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
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.StreamItem
|
||||
import com.github.libretube.constants.IntentData
|
||||
@ -382,7 +382,7 @@ class PlaylistFragment : DynamicLayoutManagerFragment(R.layout.fragment_playlist
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
// load locally stored playlists with the auth api
|
||||
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage!!)
|
||||
MediaServiceRepository.instance.getPlaylistNextPage(playlistId, nextPage!!)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
context?.toastFromMainDispatcher(e.localizedMessage.orEmpty())
|
||||
|
@ -12,7 +12,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.map
|
||||
import androidx.navigation.fragment.findNavController
|
||||
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.PreferenceKeys
|
||||
import com.github.libretube.databinding.FragmentSearchSuggestionsBinding
|
||||
@ -102,7 +102,7 @@ class SearchSuggestionsFragment : Fragment(R.layout.fragment_search_suggestions)
|
||||
lifecycleScope.launch {
|
||||
val response = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
RetrofitInstance.api.getSuggestions(query)
|
||||
MediaServiceRepository.instance.getSuggestions(query)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG(), e.toString())
|
||||
|
@ -4,8 +4,8 @@ import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.PlaylistsHelper
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.SubscriptionHelper
|
||||
import com.github.libretube.api.obj.Playlists
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
@ -78,7 +78,7 @@ class HomeViewModel : ViewModel() {
|
||||
|
||||
runSafely(
|
||||
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.viewModelScope
|
||||
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.extensions.TAG
|
||||
import com.github.libretube.helpers.LocaleHelper
|
||||
@ -28,7 +28,7 @@ class TrendsViewModel : ViewModel() {
|
||||
try {
|
||||
val region = LocaleHelper.getTrendingRegion(context)
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
RetrofitInstance.api.getTrending(region).deArrow()
|
||||
MediaServiceRepository.instance.getTrending(region).deArrow()
|
||||
}
|
||||
trendingVideos.postValue(response)
|
||||
} catch (e: IOException) {
|
||||
|
@ -77,7 +77,7 @@ class WelcomeViewModel(
|
||||
|
||||
private fun refreshAndNavigate() {
|
||||
// refresh the api urls since they have changed likely
|
||||
RetrofitInstance.lazyMgr.reset()
|
||||
RetrofitInstance.apiLazyMgr.reset()
|
||||
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.PagingState
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.Comment
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class CommentPagingSource(
|
||||
private val videoId: String,
|
||||
@ -13,9 +15,11 @@ class CommentPagingSource(
|
||||
|
||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, Comment> {
|
||||
return try {
|
||||
val result = params.key?.let {
|
||||
RetrofitInstance.api.getCommentsNextPage(videoId, it)
|
||||
} ?: RetrofitInstance.api.getComments(videoId)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
params.key?.let {
|
||||
MediaServiceRepository.instance.getCommentsNextPage(videoId, it)
|
||||
} ?: MediaServiceRepository.instance.getComments(videoId)
|
||||
}
|
||||
|
||||
if (result.commentCount > 0) onCommentCount(result.commentCount)
|
||||
|
||||
|
@ -2,8 +2,10 @@ package com.github.libretube.ui.models.sources
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.obj.Comment
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class CommentRepliesPagingSource(
|
||||
private val videoId: String,
|
||||
@ -14,7 +16,9 @@ class CommentRepliesPagingSource(
|
||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, Comment> {
|
||||
return try {
|
||||
val key = params.key.orEmpty().ifEmpty { originalComment.repliesPage.orEmpty() }
|
||||
val result = RetrofitInstance.api.getCommentsNextPage(videoId, key)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
MediaServiceRepository.instance.getCommentsNextPage(videoId, key)
|
||||
}
|
||||
|
||||
val replies = result.comments.toMutableList()
|
||||
if (params.key.isNullOrEmpty()) {
|
||||
|
@ -2,9 +2,11 @@ package com.github.libretube.ui.models.sources
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
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.util.deArrow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SearchPagingSource(
|
||||
private val searchQuery: String,
|
||||
@ -14,9 +16,14 @@ class SearchPagingSource(
|
||||
|
||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, ContentItem> {
|
||||
return try {
|
||||
val result = params.key?.let {
|
||||
RetrofitInstance.api.getSearchResultsNextPage(searchQuery, searchFilter, it)
|
||||
} ?: RetrofitInstance.api.getSearchResults(searchQuery, searchFilter)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
params.key?.let {
|
||||
MediaServiceRepository.instance.getSearchResultsNextPage(
|
||||
searchQuery, searchFilter, it
|
||||
)
|
||||
} ?: MediaServiceRepository.instance.getSearchResults(searchQuery, searchFilter)
|
||||
}
|
||||
|
||||
LoadResult.Page(result.items.deArrow(), null, result.nextpage)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
|
@ -11,6 +11,7 @@ import androidx.preference.SwitchPreferenceCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.InstanceRepository
|
||||
import com.github.libretube.api.PipedMediaServiceRepository
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.obj.PipedInstance
|
||||
import com.github.libretube.constants.IntentData
|
||||
@ -68,13 +69,13 @@ class InstanceSettings : BasePreferenceFragment() {
|
||||
}
|
||||
|
||||
authInstance.setOnPreferenceChangeListener { _, _ ->
|
||||
RetrofitInstance.lazyMgr.reset()
|
||||
RetrofitInstance.apiLazyMgr.reset()
|
||||
logoutAndUpdateUI()
|
||||
true
|
||||
}
|
||||
|
||||
authInstanceToggle.setOnPreferenceChangeListener { _, _ ->
|
||||
RetrofitInstance.lazyMgr.reset()
|
||||
RetrofitInstance.apiLazyMgr.reset()
|
||||
logoutAndUpdateUI()
|
||||
true
|
||||
}
|
||||
@ -147,7 +148,7 @@ class InstanceSettings : BasePreferenceFragment() {
|
||||
|
||||
// add the currently used instances to the list if they're currently down / not part
|
||||
// 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 }) {
|
||||
val origin = apiUrl.toHttpUrl().host
|
||||
instances.add(PipedInstance(origin, apiUrl, isCurrentlyDown = true))
|
||||
@ -215,7 +216,7 @@ class InstanceSettings : BasePreferenceFragment() {
|
||||
if (!authInstanceToggle.isChecked) {
|
||||
logoutAndUpdateUI()
|
||||
}
|
||||
RetrofitInstance.lazyMgr.reset()
|
||||
RetrofitInstance.apiLazyMgr.reset()
|
||||
ActivityCompat.recreate(requireActivity())
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.core.os.bundleOf
|
||||
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.enums.ShareObjectType
|
||||
import com.github.libretube.extensions.TAG
|
||||
@ -66,7 +66,7 @@ class ChannelOptionsBottomSheet : BaseBottomSheet() {
|
||||
R.string.play_latest_videos -> {
|
||||
try {
|
||||
val channel = withContext(Dispatchers.IO) {
|
||||
RetrofitInstance.api.getChannel(channelId)
|
||||
MediaServiceRepository.instance.getChannel(channelId)
|
||||
}
|
||||
channel.relatedStreams.firstOrNull()?.url?.toID()?.let {
|
||||
NavigationHelper.navigateVideo(
|
||||
@ -83,7 +83,7 @@ class ChannelOptionsBottomSheet : BaseBottomSheet() {
|
||||
R.string.playOnBackground -> {
|
||||
try {
|
||||
val channel = withContext(Dispatchers.IO) {
|
||||
RetrofitInstance.api.getChannel(channelId)
|
||||
MediaServiceRepository.instance.getChannel(channelId)
|
||||
}
|
||||
channel.relatedStreams.firstOrNull()?.url?.toID()?.let {
|
||||
BackgroundHelper.playOnBackground(
|
||||
|
@ -3,8 +3,8 @@ package com.github.libretube.ui.sheets
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.PlaylistsHelper
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.db.DatabaseHolder
|
||||
import com.github.libretube.enums.ImportFormat
|
||||
@ -176,7 +176,7 @@ class PlaylistOptionsBottomSheet : BaseBottomSheet() {
|
||||
DatabaseHolder.Database.playlistBookmarkDao().deleteById(playlistId)
|
||||
} else {
|
||||
val bookmark = try {
|
||||
RetrofitInstance.api.getPlaylist(playlistId)
|
||||
MediaServiceRepository.instance.getPlaylist(playlistId)
|
||||
} catch (e: Exception) {
|
||||
return@withContext
|
||||
}.toPlaylistBookmark(playlistId)
|
||||
|
@ -1,10 +1,11 @@
|
||||
package com.github.libretube.util
|
||||
|
||||
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.DeArrowContent
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.api.obj.StreamItem.Companion.TYPE_STREAM
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.extensions.toID
|
||||
@ -26,7 +27,7 @@ object DeArrowUtil {
|
||||
val videoIdsString = videoIds.mapTo(TreeSet()) { it }.joinToString(",")
|
||||
|
||||
return try {
|
||||
RetrofitInstance.api.getDeArrowContent(videoIdsString)
|
||||
MediaServiceRepository.instance.getDeArrowContent(videoIdsString)
|
||||
} catch (e: Exception) {
|
||||
Log.e(this::class.java.name, "Failed to fetch DeArrow content: ${e.message}")
|
||||
null
|
||||
@ -89,7 +90,7 @@ object DeArrowUtil {
|
||||
if (!PreferenceHelper.getBoolean(PreferenceKeys.DEARROW, false)) return contentItems
|
||||
|
||||
val videoIds = contentItems
|
||||
.filter { it.type == "stream" }
|
||||
.filter { it.type == TYPE_STREAM }
|
||||
.map { it.url.toID() }
|
||||
|
||||
if (videoIds.isEmpty()) return contentItems
|
||||
|
@ -1,9 +1,8 @@
|
||||
package com.github.libretube.util
|
||||
|
||||
import androidx.media3.common.Player
|
||||
import com.github.libretube.api.MediaServiceRepository
|
||||
import com.github.libretube.api.PlaylistsHelper
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.StreamsExtractor
|
||||
import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.extensions.move
|
||||
import com.github.libretube.extensions.runCatchingIO
|
||||
@ -158,7 +157,7 @@ object PlayingQueue {
|
||||
) {
|
||||
var playlistNextPage = nextPage
|
||||
while (playlistNextPage != null) {
|
||||
RetrofitInstance.api.getPlaylistNextPage(playlistId, playlistNextPage).run {
|
||||
MediaServiceRepository.instance.getPlaylistNextPage(playlistId, playlistNextPage).run {
|
||||
addToQueueAsync(relatedStreams, isMainList = isMainList)
|
||||
playlistNextPage = this.nextpage
|
||||
}
|
||||
@ -177,7 +176,7 @@ object PlayingQueue {
|
||||
var channelNextPage = nextPage
|
||||
var pageIndex = 1
|
||||
while (channelNextPage != null && pageIndex < 10) {
|
||||
RetrofitInstance.api.getChannelNextPage(channelId, channelNextPage).run {
|
||||
MediaServiceRepository.instance.getChannelNextPage(channelId, channelNextPage).run {
|
||||
addToQueueAsync(relatedStreams)
|
||||
channelNextPage = this.nextpage
|
||||
pageIndex++
|
||||
@ -186,14 +185,14 @@ object PlayingQueue {
|
||||
}
|
||||
|
||||
private fun insertChannel(channelId: String, newCurrentStream: StreamItem) = runCatchingIO {
|
||||
val channel = RetrofitInstance.api.getChannel(channelId)
|
||||
val channel = MediaServiceRepository.instance.getChannel(channelId)
|
||||
addToQueueAsync(channel.relatedStreams, newCurrentStream)
|
||||
if (channel.nextpage == null) return@runCatchingIO
|
||||
fetchMoreFromChannel(channelId, channel.nextpage)
|
||||
}.let { queueJobs.add(it) }
|
||||
|
||||
fun insertByVideoId(videoId: String) = runCatchingIO {
|
||||
val streams = StreamsExtractor.extractStreams(videoId.toID())
|
||||
val streams = MediaServiceRepository.instance.getStreams(videoId.toID())
|
||||
add(streams.toStreamItem(videoId))
|
||||
}
|
||||
|
||||
|
@ -472,6 +472,7 @@
|
||||
<string name="crashlog">Crashlog</string>
|
||||
<string name="never_show_again">Never show this again</string>
|
||||
<string name="update_information">Update information</string>
|
||||
<string name="mode_of_operation">Mode of operation</string>
|
||||
|
||||
<!-- Backup & Restore Settings -->
|
||||
<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="show_upcoming_videos">Show upcoming videos</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 -->
|
||||
<string name="download_channel_name">Download Service</string>
|
||||
|
@ -2,6 +2,26 @@
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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">
|
||||
|
||||
<ListPreference
|
||||
@ -10,19 +30,41 @@
|
||||
app:entries="@array/instances"
|
||||
app:entryValues="@array/instancesValue"
|
||||
app:key="selectInstance"
|
||||
android:dependency="full_local_mode"
|
||||
app:title="@string/instances" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_add_instance"
|
||||
app:key="customInstance"
|
||||
android:dependency="full_local_mode"
|
||||
app:summary="@string/customInstance_summary"
|
||||
app:title="@string/customInstance" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_trash"
|
||||
app:key="clearCustomInstances"
|
||||
android:dependency="full_local_mode"
|
||||
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
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_auth"
|
||||
@ -39,21 +81,6 @@
|
||||
app:key="selectAuthInstance"
|
||||
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
|
||||
android:icon="@drawable/ic_login_filled"
|
||||
android:summary="@string/notgmail"
|
||||
@ -75,42 +102,27 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/proxy">
|
||||
<PreferenceCategory app:title="@string/audio_video">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/ic_server"
|
||||
android:summary="@string/disable_proxy_summary"
|
||||
android:title="@string/disable_proxy"
|
||||
app:key="disable_video_image_proxy" />
|
||||
android:icon="@drawable/ic_list"
|
||||
android:summary="@string/hls_instead_of_dash_summary"
|
||||
android:title="@string/hls_instead_of_dash"
|
||||
app:key="use_hls" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:icon="@drawable/ic_region"
|
||||
android:summary="@string/local_stream_extraction_summary"
|
||||
android:title="@string/local_stream_extraction"
|
||||
android:dependency="disable_video_image_proxy"
|
||||
app:key="local_stream_extraction" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/misc">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:icon="@drawable/ic_dislike"
|
||||
android:dependency="local_stream_extraction"
|
||||
android:summary="@string/local_ryd_summary"
|
||||
android:title="@string/local_ryd"
|
||||
android:dependency="local_stream_extraction"
|
||||
app:key="local_return_youtube_dislikes" />
|
||||
|
||||
</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>
|
Loading…
x
Reference in New Issue
Block a user