3jackdaws / soundcloud-lib

Soundcloud API wrapper for tracks & playlists that doesn't require API credentials. Asyncio support.
MIT License
94 stars 24 forks source link

no support for 'https://on.soundcloud.com' type links #44

Closed Aden720 closed 3 weeks ago

Aden720 commented 3 months ago

on soundcloud mobile you can share a track or playlist, tapping copy link will generate a link with some metadata in it e.g. https://soundcloud.com/shift-key/on-my-mind -> https://on.soundcloud.com/QnSJh

when you navigate to one of these links it will send a http 302 with the full soundcloud url in the location header. currently passing one of these links to resolve raises a 404 error which I didn't expect

from sclib import SoundcloudAPI, Track

api = SoundcloudAPI()
track = api.resolve('https://on.soundcloud.com/QnSJh')

assert type(track) is Track

filename = f'./{track.artist} - {track.title}.mp3'
print(filename)

returns an error

<class 'urllib.error.HTTPError'> HTTP Error 404: Not Found
Traceback (most recent call last):
  File "/user\AppData\Local\Programs\Python\Python312\Lib\runpy.py", line 198, in _run_module_as_main
    return _run_code(code, main_globals, None,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/user\AppData\Local\Programs\Python\Python312\Lib\runpy.py", line 88, in _run_code
    exec(code, run_globals)
  File "/user\.vscode\extensions\ms-python.debugpy-2024.10.0-win32-x64\bundled\libs\debugpy\adapter/../..\debugpy\launcher/../..\debugpy\__main__.py", line 39, in <module>
    cli.main()
  File "/user\.vscode\extensions\ms-python.debugpy-2024.10.0-win32-x64\bundled\libs\debugpy\adapter/../..\debugpy\launcher/../..\debugpy/..\debugpy\server\cli.py", line 430, in main
    run()
  File "/user\.vscode\extensions\ms-python.debugpy-2024.10.0-win32-x64\bundled\libs\debugpy\adapter/../..\debugpy\launcher/../..\debugpy/..\debugpy\server\cli.py", line 284, in run_file
    runpy.run_path(target, run_name="__main__")
  File "/user\.vscode\extensions\ms-python.debugpy-2024.10.0-win32-x64\bundled\libs\debugpy\_vendored\pydevd\_pydevd_bundle\pydevd_runpy.py", line 321, in run_path
    return _run_module_code(code, init_globals, run_name,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/user\.vscode\extensions\ms-python.debugpy-2024.10.0-win32-x64\bundled\libs\debugpy\_vendored\pydevd\_pydevd_bundle\pydevd_runpy.py", line 135, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "/user\.vscode\extensions\ms-python.debugpy-2024.10.0-win32-x64\bundled\libs\debugpy\_vendored\pydevd\_pydevd_bundle\pydevd_runpy.py", line 124, in _run_code
    exec(code, run_globals)
  File "example.py", line 4, in <module>
    track = api.resolve('https://on.soundcloud.com/QnSJh')
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "d:\Downloads\pythontemp\.venv\Lib\site-packages\sclib\sync.py", line 84, in resolve
    if obj['kind'] == 'track':
       ~~~^^^^^^^^
TypeError: 'bool' object is not subscriptable
degendemolisher commented 1 month ago

using an intermediate function to grab track id first then feed into a different url to request api seems to do the job.

I modified the resolve function and added a new get_track_id function in the SoundCloudAPI class do note that I did it on the async side in asyncio.py, if you want to use the normal class then change it in sync.py below is the code I changed for asyncio.py

import aiohttp
from bs4 import BeautifulSoup

async def resolve(self, url):  # pylint: disable=invalid-overridden-method
    """ Resolve an api url to a soundcloud object """
    if not self.client_id:
        await self.get_credentials()
    full_url = f"https://api-v2.soundcloud.com/tracks?ids={await self.get_track_id(url)}&client_id={self.client_id}"
    obj = await get_obj_from(full_url)
    if not obj:
        raise ValueError('Could not resolve url')
    obj = obj[0]
    if obj['kind'] == 'track':
        return Track(obj=obj, client=self)

    if obj['kind'] in ('playlist', 'system-playlist'):
        playlist = Playlist(obj=obj, client=self)
        await playlist.clean_attributes()
        return playlist

# Getting the track ID
async def get_track_id(self, url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            content = await response.text()
            soup = BeautifulSoup(content, "html.parser")
            track_id = soup.find("meta", property="twitter:app:url:googleplay")["content"].split(":")[-1]
            return track_id

for a non-async in sync.py:

def resolve(self, url):
    """ Resolve url """
    if not self.client_id:
        self.get_credentials()
    url = f"https://api-v2.soundcloud.com/tracks?ids={self.get_track_id(url)}&client_id={self.client_id}"

    obj = get_obj_from(url)[0]
    if obj['kind'] == 'track':
        return Track(obj=obj, client=self)
    if obj['kind'] in ('playlist', 'system-playlist'):
        playlist = Playlist(obj=obj, client=self)
        playlist.clean_attributes()
        return playlist
    return None

def get_track_id(self, url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, "html.parser")
    track_id = soup.find("meta", property="twitter:app:url:googleplay")["content"].split(":")[-1]
    return track_id
Aden720 commented 1 month ago

Thanks for your response, is this a change you will be implementing to handle these URLs?

degendemolisher commented 1 month ago

I opened a pull request #45 with a better solution since my solution above broke playlist links it simply tries to get the redirect url (soundcloud.com) from on.soundcloud links then feeds into the resolve url