mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
fix(player): harden StreamProxy Origin header, YouTube URL regex boundaries, and candidate fallback order
Co-Authored-By: pgwiz <pgwiz@users.noreply.github.com>
This commit is contained in:
parent
e4fe197184
commit
f622b4b155
10
CHANGELOG.md
10
CHANGELOG.md
@ -3,11 +3,11 @@
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- **server/StreamProxy**: Prevent GoogleVideo CDN HTTP 403 Forbidden by injecting `Referer: https://www.youtube.com/` and browser `User-Agent` headers on `googlevideo.com` and `youtube.com` proxy requests, preserving stream range headers (`Range`, `If-Range`) and filtering hop-by-hop headers.
|
- **server/StreamProxy**: Prevent GoogleVideo CDN HTTP 403 Forbidden by injecting `Referer: https://www.youtube.com/`, `Origin: https://www.youtube.com`, and browser `User-Agent` headers on `googlevideo.com`, `youtube.com`, and `youtu.be` proxy requests, preserving stream range headers (`Range`, `If-Range`) without header duplication, and filtering hop-by-hop headers.
|
||||||
- **newpipe/RealNewPipeAudioAPI**: Fix YouTube video ID extraction when `track.externalUri` contains YouTube URLs (`v=`, `youtu.be`, `/embed/`, `/shorts/`) instead of passing raw Spotify track IDs to video info lookups.
|
- **newpipe/RealNewPipeAudioAPI**: Fix YouTube video ID extraction when `track.externalUri` contains YouTube URLs (`v=`, `youtu.be`, `/embed/`, `/shorts/`, with trailing slash/query/fragment support) instead of passing raw Spotify track IDs to video info lookups.
|
||||||
- **newpipe/RealNewPipeAudioAPI**: Improve track matching and scoring for Topic channels, VEVO uploads, and official variants with relaxed duration bins, and fall back to top relevance search result when confidence scoring yields no candidates $\ge 0.3$.
|
- **newpipe/RealNewPipeAudioAPI**: Improve track matching and scoring for Topic channels, VEVO uploads, and official variants with relaxed duration bins, and guarantee top relevance search result is prioritized at index 0 when confidence scoring yields no candidates $\ge 0.3$.
|
||||||
- **newpipe/NewPipeService**: Implement native YouTube radio mix extractor (`RD{videoId}`) across commonMain, androidJvmMain, and iosMain platforms.
|
- **newpipe/NewPipeService**: Implement native YouTube radio mix extractor (`RD{videoId}`) across commonMain, androidJvmMain, and iosMain platforms with safe thumbnail fallbacks.
|
||||||
- **audioplayer/DeviceAudioPlayerQueue**: Add native YouTube radio mix fallback for endless playback queue continuation when metadata recommendations fail or are unauthenticated.
|
- **audioplayer/DeviceAudioPlayerQueue**: Add native YouTube radio mix fallback for endless playback queue continuation when metadata recommendations fail or are unauthenticated, supporting complete YouTube URL patterns.
|
||||||
|
|
||||||
## [6.0.0-beta.0](https://github.com/team-spotube/spotube/compare/v5.1.2...v6.0.0-beta.0) (2026-10-26)
|
## [6.0.0-beta.0](https://github.com/team-spotube/spotube/compare/v5.1.2...v6.0.0-beta.0) (2026-10-26)
|
||||||
|
|
||||||
|
|||||||
@ -355,8 +355,9 @@ class DeviceAudioPlayerQueue(
|
|||||||
seed.id
|
seed.id
|
||||||
} else {
|
} else {
|
||||||
val extracted = seed.externalUri?.let { uri ->
|
val extracted = seed.externalUri?.let { uri ->
|
||||||
Regex("""[?&]v=([a-zA-Z0-9_-]{11})""").find(uri)?.groupValues?.get(1)
|
Regex("""[?&]v=([a-zA-Z0-9_-]{11})(?:[&#]|$)""").find(uri)?.groupValues?.get(1)
|
||||||
?: Regex("""youtu\.be/([a-zA-Z0-9_-]{11})""").find(uri)?.groupValues?.get(1)
|
?: Regex("""youtu\.be/([a-zA-Z0-9_-]{11})(?:[?#/]|$)""").find(uri)?.groupValues?.get(1)
|
||||||
|
?: Regex("""youtube\.com/(?:embed|v|shorts)/([a-zA-Z0-9_-]{11})(?:[?#/]|$)""").find(uri)?.groupValues?.get(1)
|
||||||
}
|
}
|
||||||
extracted ?: runCatching {
|
extracted ?: runCatching {
|
||||||
val artist = seed.artists.firstOrNull()?.name.orEmpty()
|
val artist = seed.artists.firstOrNull()?.name.orEmpty()
|
||||||
|
|||||||
@ -580,8 +580,10 @@ internal class StreamProxy(
|
|||||||
val host = runCatching { Url(url).host.lowercase() }.getOrNull() ?: ""
|
val host = runCatching { Url(url).host.lowercase() }.getOrNull() ?: ""
|
||||||
return host.endsWith("googlevideo.com") ||
|
return host.endsWith("googlevideo.com") ||
|
||||||
host.endsWith("youtube.com") ||
|
host.endsWith("youtube.com") ||
|
||||||
|
host.endsWith("youtu.be") ||
|
||||||
url.contains("googlevideo.com", ignoreCase = true) ||
|
url.contains("googlevideo.com", ignoreCase = true) ||
|
||||||
url.contains("youtube.com", ignoreCase = true)
|
url.contains("youtube.com", ignoreCase = true) ||
|
||||||
|
url.contains("youtu.be", ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyUpstreamHeaders(
|
private fun applyUpstreamHeaders(
|
||||||
@ -598,6 +600,7 @@ internal class StreamProxy(
|
|||||||
|
|
||||||
if (isYouTubeOrGoogleVideo(targetUrl)) {
|
if (isYouTubeOrGoogleVideo(targetUrl)) {
|
||||||
builder.headers[HttpHeaders.Referrer] = YOUTUBE_REFERER
|
builder.headers[HttpHeaders.Referrer] = YOUTUBE_REFERER
|
||||||
|
builder.headers[HttpHeaders.Origin] = "https://www.youtube.com"
|
||||||
builder.headers[HttpHeaders.UserAgent] = BROWSER_USER_AGENT
|
builder.headers[HttpHeaders.UserAgent] = BROWSER_USER_AGENT
|
||||||
} else {
|
} else {
|
||||||
call.forwardRequestHeaderIfPresent(builder.headers, HttpHeaders.UserAgent)
|
call.forwardRequestHeaderIfPresent(builder.headers, HttpHeaders.UserAgent)
|
||||||
@ -610,7 +613,7 @@ internal class StreamProxy(
|
|||||||
headerName: String
|
headerName: String
|
||||||
) {
|
) {
|
||||||
request.headers.getAll(headerName)?.forEach { value ->
|
request.headers.getAll(headerName)?.forEach { value ->
|
||||||
builder.append(headerName, value)
|
builder[headerName] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -148,10 +148,10 @@ class RealNewPipeAudioAPI : AudioAPI, KoinComponent {
|
|||||||
val vParamRegex = Regex("""[?&]v=([a-zA-Z0-9_-]{11})(?:[&#]|$)""")
|
val vParamRegex = Regex("""[?&]v=([a-zA-Z0-9_-]{11})(?:[&#]|$)""")
|
||||||
vParamRegex.find(trimmed)?.groupValues?.get(1)?.let { return it }
|
vParamRegex.find(trimmed)?.groupValues?.get(1)?.let { return it }
|
||||||
|
|
||||||
val youtuBeRegex = Regex("""youtu\.be/([a-zA-Z0-9_-]{11})(?:[?#]|$)""")
|
val youtuBeRegex = Regex("""youtu\.be/([a-zA-Z0-9_-]{11})(?:[?#/]|$)""")
|
||||||
youtuBeRegex.find(trimmed)?.groupValues?.get(1)?.let { return it }
|
youtuBeRegex.find(trimmed)?.groupValues?.get(1)?.let { return it }
|
||||||
|
|
||||||
val pathRegex = Regex("""youtube\.com/(?:embed|v|shorts)/([a-zA-Z0-9_-]{11})(?:[?#]|$)""")
|
val pathRegex = Regex("""youtube\.com/(?:embed|v|shorts)/([a-zA-Z0-9_-]{11})(?:[?#/]|$)""")
|
||||||
pathRegex.find(trimmed)?.groupValues?.get(1)?.let { return it }
|
pathRegex.find(trimmed)?.groupValues?.get(1)?.let { return it }
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@ -266,9 +266,10 @@ class RealNewPipeAudioAPI : AudioAPI, KoinComponent {
|
|||||||
// When confidence scoring yields no candidates >= 0.3, ensure the top relevance search
|
// When confidence scoring yields no candidates >= 0.3, ensure the top relevance search
|
||||||
// result is returned as fallback rather than throwing TrackNotFoundError or failing.
|
// result is returned as fallback rather than throwing TrackNotFoundError or failing.
|
||||||
val fallback = if (textSearchResults.isNotEmpty()) {
|
val fallback = if (textSearchResults.isNotEmpty()) {
|
||||||
textSearchResults.values.toList()
|
val list = textSearchResults.values.toList()
|
||||||
.sortedByDescending { it.confidence } // stable: ties keep YouTube's relevance order
|
val topRelevance = list.first()
|
||||||
.take(5)
|
val rest = list.drop(1).sortedByDescending { it.confidence }
|
||||||
|
(listOf(topRelevance) + rest).take(5)
|
||||||
} else if (ranked.isNotEmpty()) {
|
} else if (ranked.isNotEmpty()) {
|
||||||
ranked.take(5)
|
ranked.take(5)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -100,9 +100,11 @@ actual class NewPipeService {
|
|||||||
url = item.url,
|
url = item.url,
|
||||||
uploader = item.uploaderName ?: "Unknown",
|
uploader = item.uploaderName ?: "Unknown",
|
||||||
durationMs = secondsToMs(item.duration),
|
durationMs = secondsToMs(item.duration),
|
||||||
thumbnailUrl = item.thumbnails.first().url,
|
thumbnailUrl = item.thumbnails.firstOrNull()?.url.orEmpty(),
|
||||||
id = Url(item.url).parameters["v"]
|
id = runCatching { Url(item.url).parameters["v"] }.getOrNull()
|
||||||
?: item.url.substringAfter("watch?v=", "").substringBefore("&")
|
?: item.url.substringAfter("watch?v=", "").substringBefore("&")
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
?: item.url.substringAfterLast("/")
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user