Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: local streams extraction #6381

Merged
merged 1 commit into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -10,6 +10,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 @@ -57,7 +58,7 @@ class PlayerViewModel : ViewModel() {
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
Loading