Merge pull request #7151 from Bnyro/local-npe

feat: add support for using local NewPipe Extractor
This commit is contained in:
Bnyro 2025-03-03 13:33:08 +01:00 committed by GitHub
commit d7f874f5da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 927 additions and 459 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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()
}
}
}

View File

@ -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() }
)
}
}

View File

@ -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
} }

View 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
}

View File

@ -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)
}
}
}

View File

@ -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()

View File

@ -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>()
} }

View File

@ -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()
}
}
}

View File

@ -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(),

View File

@ -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
) )

View File

@ -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"

View File

@ -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,

View File

@ -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)
}
} }
} }
} }

View File

@ -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()

View File

@ -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

View File

@ -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"]"""

View File

@ -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)

View File

@ -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) {

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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()
} }

View File

@ -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()
} }

View File

@ -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())

View File

@ -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())

View File

@ -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) }
) )
} }

View File

@ -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) {

View File

@ -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)
} }

View File

@ -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)

View File

@ -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()) {

View File

@ -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)

View File

@ -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())
} }

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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))
} }

View File

@ -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>

View File

@ -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>