Skip to content

Commit

Permalink
Merge pull request #6381 from Bnyro/master
Browse files Browse the repository at this point in the history
feat: local streams extraction
  • Loading branch information
Bnyro authored Aug 23, 2024
2 parents 07f6013 + 83f0823 commit 884d9e0
Show file tree
Hide file tree
Showing 19 changed files with 214 additions and 10 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ dependencies {
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.retrofit)

/* NewPipe Extractor */
implementation(libs.newpipeextractor)

/* Cronet and Coil */
coreLibraryDesugaring(libs.desugaring)
implementation(libs.cronet.embedded)
Expand Down
6 changes: 6 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,9 @@

# Settings fragments are loaded through reflection
-keep class com.github.libretube.ui.preferences.** { *; }

## Rules for NewPipeExtractor
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.classfile.ClassFileWriter
-dontwarn org.mozilla.javascript.tools.**
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ object PlaylistsHelper {
MAX_CONCURRENT_IMPORT_CALLS
).map { videos ->
videos.parallelMap {
runCatching { RetrofitInstance.api.getStreams(it) }
runCatching { StreamsExtractor.extractStreams(it) }
.getOrNull()
?.toStreamItem(it)
}.filterNotNull()
Expand Down
108 changes: 108 additions & 0 deletions app/src/main/java/com/github/libretube/api/StreamsExtractor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.github.libretube.api

import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.api.obj.MetaInfo
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.helpers.PlayerHelper
import com.github.libretube.util.NewPipeDownloaderImpl
import kotlinx.datetime.toKotlinInstant
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem

object StreamsExtractor {
// val npe by lazy {
// NewPipe.getService(ServiceList.YouTube.serviceId)
// }

init {
NewPipe.init(NewPipeDownloaderImpl())
}

suspend fun extractStreams(videoId: String): Streams {
if (!PlayerHelper.disablePipedProxy || !PlayerHelper.localStreamExtraction) {
return RetrofitInstance.api.getStreams(videoId)
}

val resp = StreamInfo.getInfo("https://www.youtube.com/watch?v=$videoId")
return Streams(
title = resp.name,
description = resp.description.toString(),
uploader = resp.uploaderName,
uploaderAvatar = resp.uploaderAvatars.maxBy { it.height }.url,
uploaderUrl = resp.uploaderUrl,
uploaderVerified = resp.isUploaderVerified,
uploaderSubscriberCount = resp.uploaderSubscriberCount,
category = resp.category,
views = resp.viewCount,
likes = resp.likeCount,
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(),
thumbnailUrl = resp.thumbnails.maxBy { it.height }.url,
relatedStreams = resp.relatedItems.map { it as StreamInfoItem }.map {
StreamItem(
it.url,
it.infoType.name,
it.name,
it.thumbnails.maxBy { image -> image.height }.url,
it.uploaderName,
it.uploaderUrl,
it.uploaderAvatars.maxBy { image -> image.height }.url,
it.textualUploadDate,
it.duration,
it.viewCount,
it.isUploaderVerified,
it.uploadDate?.offsetDateTime()?.toEpochSecond() ?: 0L,
it.shortDescription,
it.isShortFormContent,
)
},
chapters = resp.streamSegments.map {
ChapterSegment(
title = it.title,
image = it.previewUrl.orEmpty(),
start = it.startTimeSeconds.toLong()
)
},
audioStreams = emptyList(), // TODO: audio streams and video streams via DASH, currently broken anyways
videoStreams = emptyList(),
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
)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,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 LOCAL_STREAM_EXTRACTION = "local_stream_extraction"

// History
const val WATCH_HISTORY_SIZE = "watch_history_size"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,12 @@ object PlayerHelper {
false
)

val localStreamExtraction: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.LOCAL_STREAM_EXTRACTION,
true
)

val useHlsOverDash: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.USE_HLS_OVER_DASH,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ object ProxyHelper {
* Detect whether the proxy should be used or not for a given stream URL based on user preferences
*/
fun unwrapStreamUrl(url: String): String {
return if (PlayerHelper.disablePipedProxy) {
return if (PlayerHelper.disablePipedProxy && !PlayerHelper.localStreamExtraction) {
unwrapUrl(url)
} else {
url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.github.libretube.LibreTubeApp.Companion.DOWNLOAD_CHANNEL_NAME
import com.github.libretube.R
import com.github.libretube.api.CronetHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.StreamsExtractor
import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.Download
Expand Down Expand Up @@ -109,7 +110,7 @@ class DownloadService : LifecycleService() {
lifecycleScope.launch(coroutineContext) {
try {
val streams = withContext(Dispatchers.IO) {
RetrofitInstance.api.getStreams(videoId)
StreamsExtractor.extractStreams(videoId)
}

val thumbnailTargetPath = getDownloadPath(DownloadHelper.THUMBNAIL_DIR, fileName)
Expand Down Expand Up @@ -386,7 +387,7 @@ class DownloadService : LifecycleService() {
* Regenerate stream url using available info format and quality.
*/
private suspend fun regenerateLink(item: DownloadItem) {
val streams = RetrofitInstance.api.getStreams(item.videoId)
val streams = StreamsExtractor.extractStreams(item.videoId)
val stream = when (item.type) {
FileType.AUDIO -> streams.audioStreams
FileType.VIDEO -> streams.videoStreams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.github.libretube.LibreTubeApp.Companion.PLAYER_CHANNEL_NAME
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.obj.Segment
import com.github.libretube.api.obj.Streams
import com.github.libretube.constants.IntentData
Expand Down Expand Up @@ -253,7 +254,7 @@ class OnlinePlayerService : LifecycleService() {

lifecycleScope.launch(Dispatchers.IO) {
streams = runCatching {
RetrofitInstance.api.getStreams(videoId)
StreamsExtractor.extractStreams(videoId)
}.getOrNull() ?: return@launch

// clear the queue if it shouldn't be kept explicitly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.github.libretube.LibreTubeApp.Companion.PLAYLIST_DOWNLOAD_ENQUEUE_CHA
import com.github.libretube.R
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
Expand Down Expand Up @@ -136,7 +137,7 @@ class PlaylistDownloadEnqueueService : LifecycleService() {

for (stream in streams) {
val videoInfo = runCatching {
RetrofitInstance.api.getStreams(stream.url!!.toID())
StreamsExtractor.extractStreams(stream.url!!.toID())
}.getOrNull() ?: continue

val videoStream = getStream(videoInfo.videoStreams, maxVideoQuality)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.lifecycle.repeatOnLifecycle
import com.github.libretube.R
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.StreamsExtractor
import com.github.libretube.api.obj.Playlists
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.DialogAddToPlaylistBinding
Expand Down Expand Up @@ -112,7 +113,7 @@ class AddToPlaylistDialog : DialogFragment() {
val streams = when {
videoId != null -> listOfNotNull(
runCatching {
RetrofitInstance.api.getStreams(videoId!!).toStreamItem(videoId!!)
StreamsExtractor.extractStreams(videoId!!).toStreamItem(videoId!!)
}.getOrNull()
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
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.Streams
import com.github.libretube.api.obj.Subtitle
Expand Down Expand Up @@ -81,7 +82,7 @@ class DownloadDialog : DialogFragment() {
lifecycleScope.launch {
val response = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getStreams(videoId)
StreamsExtractor.extractStreams(videoId)
}
} catch (e: IOException) {
println(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
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.obj.Message
import com.github.libretube.api.obj.Segment
import com.github.libretube.api.obj.Streams
Expand Down Expand Up @@ -53,7 +54,7 @@ class PlayerViewModel(
if (isOrientationChangeInProgress && streamsInfo != null) return@withContext streamsInfo to null

streamsInfo = try {
RetrofitInstance.api.getStreams(videoId).deArrow(videoId)
StreamsExtractor.extractStreams(videoId).deArrow(videoId)
} catch (e: IOException) {
return@withContext null to context.getString(R.string.unknown_error)
} catch (e: HttpException) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.github.libretube.util

import java.io.IOException
import java.util.concurrent.TimeUnit
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.extractor.downloader.Request
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException

class NewPipeDownloaderImpl : Downloader() {
private val client: OkHttpClient = OkHttpClient.Builder()
.readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.build()

@Throws(IOException::class, ReCaptchaException::class)
override fun execute(request: Request): Response {
val url = request.url()

val requestBody = request.dataToSend()?.let {
it.toRequestBody(APPLICATION_JSON, 0, it.size)
}

val requestBuilder = okhttp3.Request.Builder()
.method(request.httpMethod(), requestBody)
.url(url)
.addHeader(USER_AGENT_HEADER_NAME, USER_AGENT)

for ((headerName, headerValueList) in request.headers()) {
requestBuilder.removeHeader(headerName)
for (headerValue in headerValueList) {
requestBuilder.addHeader(headerName, headerValue)
}
}

val response = client.newCall(requestBuilder.build()).execute()
if (response.code == CAPTCHA_STATUS_CODE) {
response.close()
throw ReCaptchaException("reCaptcha Challenge requested", url)
}

return Response(
response.code,
response.message,
response.headers.toMultimap(),
response.body?.string(),
response.request.url.toString()
)
}

companion object {
private const val USER_AGENT_HEADER_NAME = "User-Agent"
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0"
private const val CAPTCHA_STATUS_CODE = 429
private val APPLICATION_JSON = "application/json".toMediaType()
private const val READ_TIMEOUT_SECONDS = 30L
}
}
3 changes: 2 additions & 1 deletion app/src/main/java/com/github/libretube/util/PlayingQueue.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.util.Log
import androidx.media3.common.Player
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
Expand Down Expand Up @@ -179,7 +180,7 @@ object PlayingQueue {
}

fun insertByVideoId(videoId: String) = runCatchingIO {
val streams = RetrofitInstance.api.getStreams(videoId.toID())
val streams = StreamsExtractor.extractStreams(videoId.toID())
add(streams.toStreamItem(videoId))
}

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,8 @@
<string name="gestures">Gestures</string>
<string name="external_download_provider">External download provider</string>
<string name="external_download_provider_summary">Enter the package name of the app you want to use for downloading videos. Leave blank to use LibreTube\'s inbuilt downloader.</string>
<string name="local_stream_extraction">Local stream extraction</string>
<string name="local_stream_extraction_summary">Directly fetch video playback information from YouTube without using Piped.</string>

<!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string>
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/res/xml/instance_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@
android:title="@string/disable_proxy"
app:key="disable_video_image_proxy" />

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

</PreferenceScreen>
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ allprojects {
repositories {
google()
mavenCentral()
maven { setUrl("https://jitpack.io") }
}
}

Expand Down
Loading

0 comments on commit 884d9e0

Please sign in to comment.