Soundcloud 获取直接音频 URL?

Soundcloud get direct audio URL?

我想从 SoundCloud 的 URL 获取直接音频 URL,而不使用任何第三方服务,如 soundcloudtomp3.app。有没有给定的 API 可以处理它?

这个问题不知疲倦地搜索Google几个小时后,通过在浏览器的DevTools中观察音频加载前后的网络请求,终于找到了答案。

这些步骤是:

  1. 使用此 API 从特定 URL 获取音频信息:

https://api-widget.soundcloud.com/resolve?url=SOUNDLCOUD_URL&format=json&client_id=CLIENT_ID

  1. 使用 JSONpath 获取媒体流 URL:

STREAM_URL = $.media.transcodings[1].url

Ex: https://api-v2.soundcloud.com/media/soundcloud:tracks:893500207/353bceea-97e9-461a-b57d-18a2fa7553ec/stream/progressive

  1. 获取曲目授权令牌:

TRACK_AUTHORIZATION = $.track_authorization

  1. 在上述步骤中使用此信息获取直接音频URL:

https://STREAM_URL?client_id=CLIENT_ID&track_authorization=TRACK_AUTHORIZATION

我的 java 代码。

public class SoundCloudAPIServiceImpl implements SoundCloudAPIService{
    
    private static final String SOUNDCLOUD_URL_REGEX = "^\n" +
            "((?:https?:)?//)? #protocol\n" +
            "(www\.|m\.)? #sub-domain\n" +
            "(soundcloud\.com|snd\.sc) #domain name\n" +
            "/(.*) #audio path\n" +
            "$";
    
    private static final String SOUNDCLOUD_RESOLVE_URL_API ="https://api-widget.soundcloud.com/resolve";
    private static final String REGEX_SPLIT_STRING_BY_SPACE_NOT_INSIDE_QUOTE = "([^\"]\S*|\".+?\")\s*";
    private static final String TRACK_AUTHORIZATION = "track_authorization";
    
    @Value("${soundcloud.browser.client.id}")
    private String clientId;
    
    @Override
    public GrabAudioInfo grabInfo(String soundCloudUrl) throws EkoBaseException {
        Matcher soundCloudUrlMatcher = Pattern
                .compile(SOUNDCLOUD_URL_REGEX, Pattern.COMMENTS | Pattern.CASE_INSENSITIVE)
                .matcher(soundCloudUrl);
        
        if (soundCloudUrlMatcher.find()) {
            try {
                String targetUrl = UriComponentsBuilder
                        .fromUriString(SOUNDCLOUD_RESOLVE_URL_API)
                        .queryParam(DBConst.URL, soundCloudUrl)
                        .queryParam(DBConst.FORMAT, DBConst.JSON)
                        .queryParam(DBConst.CLIENT_ID, clientId)
                        .build()
                        .toUriString();
                
                String responseJson = IOUtils.toString(new URL(targetUrl), StandardCharsets.UTF_8);
                DocumentContext documentContext = JsonPath.parse(responseJson);
                
                LinkedHashMap<Object, Object> responseMap = JsonPath.read(responseJson, "$");
                
                if (responseMap.size() > 0) {
                    String title = documentContext.read("$.title").toString();
                    String description = documentContext.read("$.description").toString();
                    List<String> tags = this.getTags(documentContext);
                    String directAudioUrl = this.getDirectAudioUrl(documentContext);
                    String artworkUrl = documentContext.read("$.artwork_url").toString();
                    String artworkOriginalSizeUrl = artworkUrl.replace("-large", "-original");
                    
                    BufferedImage bufferedImage = ImageIO.read(new URL(artworkOriginalSizeUrl));
                    Integer height = bufferedImage.getHeight();
                    Integer width = bufferedImage.getWidth();
                    
                    GrabAudioInfo grabAudioInfo = new GrabAudioInfo();
                    grabAudioInfo.setTitle(title);
                    grabAudioInfo.setDescription(description);
                    grabAudioInfo.setTags(tags);
                    grabAudioInfo.setDirectAudioUrl(directAudioUrl);
                    grabAudioInfo.setThumbnailUrl(artworkOriginalSizeUrl);
                    grabAudioInfo.setWidth(width);
                    grabAudioInfo.setHeight(height);
                    return grabAudioInfo;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            
        } else {
            log.info("Invalid SoundCloud URL: {}", soundCloudUrl);
            throw new EkoBaseException(ErrorInfo.INVALID_AUDIO_URL_ERROR);
        }
        return null;
    }
    
    private List<String> getTags(DocumentContext documentContext) {
        List<String> tags = new ArrayList<>();
        String tagListStr = documentContext.read("$.tag_list");
        Matcher m = Pattern.compile(REGEX_SPLIT_STRING_BY_SPACE_NOT_INSIDE_QUOTE).matcher(tagListStr);
        while (m.find()) {
            tags.add(m.group(1));
        }
        return tags;
    }
    
    private String getDirectAudioUrl(DocumentContext documentContext) throws IOException {
        String directAudioBaseUrl = documentContext.read("$.media.transcodings[1].url").toString();
        String trackAuthorization = documentContext.read("$.track_authorization").toString();
        String directAudioAPI = UriComponentsBuilder
                .fromUriString(directAudioBaseUrl)
                .queryParam(DBConst.CLIENT_ID, clientId)
                .queryParam(TRACK_AUTHORIZATION, trackAuthorization)
                .queryParam(DBConst.FORMAT, DBConst.JSON)
                .build()
                .toUriString();
        
        String directAudioAPIResponse = IOUtils.toString(new URL(directAudioAPI), StandardCharsets.UTF_8);
        return JsonPath.parse(directAudioAPIResponse).read("url").toString();
    }
    
    //third-party solution.
    private void convertToDirectAudioUrl(String soundCloudUrl) {
        WebDriverManager.getInstance(DriverManagerType.CHROME).setup();
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless", "--disable-gpu", "--window-size=1280,800", "--ignore-certificate-errors");
        WebDriver webDriver = new ChromeDriver(options);
        
        webDriver.get("https://soundcloudtomp3.app/");
        WebDriverWait wait = new WebDriverWait(webDriver, 15);
        
        //
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("/html/body/div[2]/div/center/form/div/input"))).sendKeys(soundCloudUrl);
        wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("/html/body/div[2]/div/center/form/div/span/button"))).click();
        WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(
                By.xpath("/html/body/div[2]/main/div/div/div[3]/div[2]/table/tbody/tr[2]/td/a"))
        );
        
        String directAudioUrl = element.getAttribute("href");
        webDriver.quit();
    }
}