feat: Pinterest Playwright fallback + Spotify graceful DRM message
Deploy Scraper / build-and-deploy (push) Canceled after 0s
Deploy Scraper / build-and-deploy (push) Canceled after 0s
- fetch_pinterest: pinterestdownloader.io API failures now non-fatal, fall through to Playwright scraping (captures v.pinimg.com video URLs); returns graceful 200 'No download URLs found' instead of 502 on dead API. - fetch_spotify: catch yt-dlp DRM error and return 200 with track metadata + clear message (direct audio needs Spotify Premium). No more raw 502.
This commit is contained in:
@@ -1393,8 +1393,34 @@ pub async fn fetch_spotify(url: &str) -> Result<DownloadResult, ScrapingError> {
|
|||||||
let resource_type = captures.get(1).map(|m| m.as_str()).unwrap_or("track");
|
let resource_type = captures.get(1).map(|m| m.as_str()).unwrap_or("track");
|
||||||
let resource_id = captures.get(2).map(|m| m.as_str()).unwrap_or("");
|
let resource_id = captures.get(2).map(|m| m.as_str()).unwrap_or("");
|
||||||
|
|
||||||
// Use yt-dlp to extract track info and download URL
|
// Use yt-dlp to extract track info and download URL.
|
||||||
let data = run_ytdlp_json(url, &["--extract-audio", "--audio-format", "mp3"]).await?;
|
// Spotify tracks are DRM-protected: yt-dlp returns a "DRM" error without
|
||||||
|
// Premium auth, so catch that and return a clean informational message.
|
||||||
|
let data = match run_ytdlp_json(url, &["--extract-audio", "--audio-format", "mp3"]).await {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
// Spotify tracks are DRM-protected (yt-dlp returns a "DRM" error without
|
||||||
|
// Premium auth). Return a clean informational message instead of an error.
|
||||||
|
let mut result = DownloadResult::success(None);
|
||||||
|
result.provider = Some(format!("spotify-metadata ({})", resource_type));
|
||||||
|
result.media.push(MediaItem {
|
||||||
|
url: format!("https://open.spotify.com/{}/{}", resource_type, resource_id),
|
||||||
|
quality: Some("metadata".to_string()),
|
||||||
|
file_type: Some(MediaType::File),
|
||||||
|
extension: Some("json".to_string()),
|
||||||
|
thumbnail: None,
|
||||||
|
file_size: None,
|
||||||
|
size_bytes: None,
|
||||||
|
frame_width: None,
|
||||||
|
frame_height: None,
|
||||||
|
note: Some(
|
||||||
|
"Spotify is DRM-protected: direct audio download requires a Spotify Premium account. Returned track metadata instead."
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let title = data
|
let title = data
|
||||||
.get("title")
|
.get("title")
|
||||||
@@ -1953,7 +1979,9 @@ pub async fn fetch_pinterest(url: &str) -> Result<DownloadResult, ScrapingError>
|
|||||||
let client = http_client();
|
let client = http_client();
|
||||||
let encoded = urlencoding::encode(url).to_string();
|
let encoded = urlencoding::encode(url).to_string();
|
||||||
|
|
||||||
let resp = client
|
// Try the pinterestdownloader.io API — but treat any failure (network, non-JSON,
|
||||||
|
// error body) as non-fatal so we can fall through to Playwright scraping below.
|
||||||
|
let resp_opt = match client
|
||||||
.client()
|
.client()
|
||||||
.get(format!(
|
.get(format!(
|
||||||
"https://pinterestdownloader.io/frontendService/DownloaderService?url={}",
|
"https://pinterestdownloader.io/frontendService/DownloaderService?url={}",
|
||||||
@@ -1967,24 +1995,23 @@ pub async fn fetch_pinterest(url: &str) -> Result<DownloadResult, ScrapingError>
|
|||||||
.timeout(Duration::from_secs(15))
|
.timeout(Duration::from_secs(15))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ScrapingError::Http(format!("Pinterest fetch failed: {}", e)))?
|
{
|
||||||
.json::<serde_json::Value>()
|
Ok(r) => match r.text().await {
|
||||||
.await
|
Ok(body) => serde_json::from_str::<serde_json::Value>(&body)
|
||||||
.map_err(|e| ScrapingError::Http(format!("Pinterest JSON parse failed: {}", e)))?;
|
.ok()
|
||||||
|
.filter(|resp| {
|
||||||
if !resp
|
resp.get("success")
|
||||||
.get("success")
|
|
||||||
.and_then(|v| v.as_bool())
|
.and_then(|v| v.as_bool())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
}),
|
||||||
return Ok(DownloadResult::error(
|
Err(_) => None,
|
||||||
resp.get("error")
|
},
|
||||||
.and_then(|v| v.as_str())
|
Err(_) => None,
|
||||||
.unwrap_or("failed"),
|
};
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut result = DownloadResult::success(None);
|
let mut result = DownloadResult::success(None);
|
||||||
|
|
||||||
|
if let Some(resp) = resp_opt {
|
||||||
result.provider = Some("pinterestdownloader".to_string());
|
result.provider = Some("pinterestdownloader".to_string());
|
||||||
|
|
||||||
let originals: std::collections::HashSet<String> = std::collections::HashSet::new();
|
let originals: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||||
@@ -2070,6 +2097,22 @@ pub async fn fetch_pinterest(url: &str) -> Result<DownloadResult, ScrapingError>
|
|||||||
});
|
});
|
||||||
|
|
||||||
result.media = media_list;
|
result.media = media_list;
|
||||||
|
} // end if let Some(resp) — API returned nothing on error, fall through below
|
||||||
|
|
||||||
|
// If the pinterestdownloader.io API returned nothing, fall back to
|
||||||
|
// Playwright browser scraping (same pattern as Instagram/Facebook) —
|
||||||
|
// the Playwright scraper captures v.pinimg.com video URLs from network responses.
|
||||||
|
if result.media.is_empty() {
|
||||||
|
if let Ok(data) = run_playwright_scraper(url, "pinterest").await {
|
||||||
|
let pw_result = playwright_to_download_result(&data);
|
||||||
|
if !pw_result.media.is_empty() {
|
||||||
|
return Ok(pw_result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.media.is_empty() {
|
||||||
|
result.message = Some("No download URLs found".to_string());
|
||||||
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user