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]
|
||||
|
||||
### 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.
|
||||
- **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**: 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/NewPipeService**: Implement native YouTube radio mix extractor (`RD{videoId}`) across commonMain, androidJvmMain, and iosMain platforms.
|
||||
- **audioplayer/DeviceAudioPlayerQueue**: Add native YouTube radio mix fallback for endless playback queue continuation when metadata recommendations fail or are unauthenticated.
|
||||
- **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/`, 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 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 with safe thumbnail fallbacks.
|
||||
- **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)
|
||||
|
||||
|
||||
@ -355,8 +355,9 @@ class DeviceAudioPlayerQueue(
|
||||
seed.id
|
||||
} else {
|
||||
val extracted = seed.externalUri?.let { uri ->
|
||||
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("""[?&]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("""youtube\.com/(?:embed|v|shorts)/([a-zA-Z0-9_-]{11})(?:[?#/]|$)""").find(uri)?.groupValues?.get(1)
|
||||
}
|
||||
extracted ?: runCatching {
|
||||
val artist = seed.artists.firstOrNull()?.name.orEmpty()
|
||||
|
||||
@ -580,8 +580,10 @@ internal class StreamProxy(
|
||||
val host = runCatching { Url(url).host.lowercase() }.getOrNull() ?: ""
|
||||
return host.endsWith("googlevideo.com") ||
|
||||
host.endsWith("youtube.com") ||
|
||||
host.endsWith("youtu.be") ||
|
||||
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(
|
||||
@ -598,6 +600,7 @@ internal class StreamProxy(
|
||||
|
||||
if (isYouTubeOrGoogleVideo(targetUrl)) {
|
||||
builder.headers[HttpHeaders.Referrer] = YOUTUBE_REFERER
|
||||
builder.headers[HttpHeaders.Origin] = "https://www.youtube.com"
|
||||
builder.headers[HttpHeaders.UserAgent] = BROWSER_USER_AGENT
|
||||
} else {
|
||||
call.forwardRequestHeaderIfPresent(builder.headers, HttpHeaders.UserAgent)
|
||||
@ -610,7 +613,7 @@ internal class StreamProxy(
|
||||
headerName: String
|
||||
) {
|
||||
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})(?:[&#]|$)""")
|
||||
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 }
|
||||
|
||||
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 }
|
||||
|
||||
return null
|
||||
@ -266,9 +266,10 @@ class RealNewPipeAudioAPI : AudioAPI, KoinComponent {
|
||||
// When confidence scoring yields no candidates >= 0.3, ensure the top relevance search
|
||||
// result is returned as fallback rather than throwing TrackNotFoundError or failing.
|
||||
val fallback = if (textSearchResults.isNotEmpty()) {
|
||||
textSearchResults.values.toList()
|
||||
.sortedByDescending { it.confidence } // stable: ties keep YouTube's relevance order
|
||||
.take(5)
|
||||
val list = textSearchResults.values.toList()
|
||||
val topRelevance = list.first()
|
||||
val rest = list.drop(1).sortedByDescending { it.confidence }
|
||||
(listOf(topRelevance) + rest).take(5)
|
||||
} else if (ranked.isNotEmpty()) {
|
||||
ranked.take(5)
|
||||
} else {
|
||||
|
||||
@ -100,9 +100,11 @@ actual class NewPipeService {
|
||||
url = item.url,
|
||||
uploader = item.uploaderName ?: "Unknown",
|
||||
durationMs = secondsToMs(item.duration),
|
||||
thumbnailUrl = item.thumbnails.first().url,
|
||||
id = Url(item.url).parameters["v"]
|
||||
thumbnailUrl = item.thumbnails.firstOrNull()?.url.orEmpty(),
|
||||
id = runCatching { Url(item.url).parameters["v"] }.getOrNull()
|
||||
?: item.url.substringAfter("watch?v=", "").substringBefore("&")
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?: item.url.substringAfterLast("/")
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
||||
Loading…
Reference in New Issue
Block a user