session.py
1 import aiohttp 2 from bs4 import BeautifulSoup 3 from pathlib import Path 4 from .user import SoundgasmUser 5 from ..base.decorators import dual_mode 6 7 8 class SoundgasmSession: 9 """ 10 Authenticated session for Soundgasm operations such as login and upload. 11 Holds an AsyncHttpClient instance that must be closed by calling close(). 12 """ 13 14 def __init__(self, http_client, username: str): 15 """ 16 Initialize the session with an HTTP client and a username. 17 """ 18 self.http = http_client 19 self.logged_in = False 20 self.username = username 21 22 async def _login(self, password: str) -> None: 23 """ 24 Authenticate the user by submitting credentials and CSRF token. 25 Raises an exception if login fails. 26 """ 27 login_page_resp = await self.http.get("https://soundgasm.net/login") 28 html = await login_page_resp.text() 29 soup = BeautifulSoup(html, "html.parser") 30 csrf_tag = soup.find("input", {"name": "authenticity_token"}) 31 if not csrf_tag or not csrf_tag.get("value"): 32 raise Exception("Login CSRF token not found") 33 csrf_token = csrf_tag["value"] 34 payload = { 35 "authenticity_token": csrf_token, 36 "user[login]": self.username, 37 "user[password]": password, 38 } 39 headers = {"Referer": "https://soundgasm.net/login"} 40 resp = await self.http.post(url="https://soundgasm.net/session", data=payload, headers=headers) 41 if "/session" in str(resp.url): 42 raise Exception("Login failed") 43 self.logged_in = True 44 45 @dual_mode 46 async def upload(self, title: str, description: str, file_path: str) -> None: 47 """ 48 Upload an audio file to the authenticated account with given title and description. 49 Requires a successful login. 50 """ 51 if not self.logged_in: 52 raise Exception("Not logged in") 53 upload_page_resp = await self.http.get("https://soundgasm.net/upload") 54 html = await upload_page_resp.text() 55 soup = BeautifulSoup(html, "html.parser") 56 csrf_tag = soup.find("input", {"name": "authenticity_token"}) 57 if not csrf_tag or not csrf_tag.get("value"): 58 raise Exception("Upload CSRF token not found") 59 csrf_token = csrf_tag["value"] 60 with open(file_path, "rb") as f: 61 data = aiohttp.FormData() 62 data.add_field("authenticity_token", csrf_token) 63 data.add_field("audio[title]", title) 64 data.add_field("audio[description]", description) 65 data.add_field("audio[audio_file]", f, filename=Path(file_path).name) 66 headers = {"Referer": "https://soundgasm.net/upload", "X-Requested-With": "XMLHttpRequest"} 67 resp = await self.http.post(url="https://soundgasm.net/audio", data=data, headers=headers) 68 if resp.status != 200: 69 raise Exception(f"Upload failed with status {resp.status}") 70 71 def get_user(self, username: str) -> SoundgasmUser: 72 """ 73 Return a user handle for read-only operations such as fetching tracks. 74 """ 75 return SoundgasmUser(username) 76 77 @dual_mode 78 async def close(self) -> None: 79 """ 80 Close the underlying HTTP client. 81 """ 82 await self.http.close()