Skip to content

Commit 64d92a2

Browse files
committed
implement all player ops + improve logging
1 parent bf784fd commit 64d92a2

File tree

3 files changed

+105
-51
lines changed

3 files changed

+105
-51
lines changed

bot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async def on_ready(self) -> None:
4040
url=f'ws://{CONFIG["SERVER"]["host"]}:{CONFIG["SERVER"]["port"]}',
4141
headers={
4242
'Authorization': CONFIG['SERVER']['password'],
43-
'User-Agent': 'Python/3.10 swish.py/v0.0.1a',
43+
'User-Agent': 'Python/v3.10.1,swish.py/v0.0.1a',
4444
'User-Id': str(self.user.id),
4545
},
4646
)
@@ -142,7 +142,7 @@ async def play(self, ctx: commands.Context, *, query: str) -> None:
142142

143143
async with self.bot.session.get(
144144
url='http://127.0.0.1:8000/search',
145-
params={"query": query},
145+
params={'query': query},
146146
) as response:
147147

148148
data = await response.json()

swish/app.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
You should have received a copy of the GNU Affero General Public License
1616
along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
"""
18+
1819
from __future__ import annotations
1920

2021
import base64
@@ -52,8 +53,6 @@ def __init__(self):
5253

5354
async def _run_app(self) -> None:
5455

55-
logger.debug('Starting Swish server...')
56-
5756
runner = aiohttp.web.AppRunner(
5857
app=self
5958
)
@@ -74,7 +73,7 @@ async def _run_app(self) -> None:
7473
async def websocket_handler(self, request: aiohttp.web.Request) -> aiohttp.web.WebSocketResponse:
7574

7675
# Initialise connection
77-
logger.info(f'Incoming websocket connection request from "{request.remote}".')
76+
logger.info(f'<{request.remote}> - Incoming websocket connection request.')
7877

7978
websocket = aiohttp.web.WebSocketResponse()
8079
await websocket.prepare(request)
@@ -83,8 +82,8 @@ async def websocket_handler(self, request: aiohttp.web.Request) -> aiohttp.web.W
8382
user_agent = request.headers.get('User-Agent')
8483

8584
if not user_agent:
86-
logger.error(f'Websocket connection from "{request.remote}" failed due to missing User-Agent header.')
87-
await websocket.close(code=4000, message=b'Missing "User-Agent" header.')
85+
logger.error(f'<{request.remote}> - Websocket connection failed due to missing \'User-Agent\' header.')
86+
await websocket.close(code=4000, message=b'Missing \'User-Agent\' header.')
8887
return websocket
8988

9089
client_name = f'{user_agent} ({request.remote})'
@@ -93,15 +92,15 @@ async def websocket_handler(self, request: aiohttp.web.Request) -> aiohttp.web.W
9392
user_id = request.headers.get('User-Id')
9493

9594
if not user_id:
96-
logger.error(f'Websocket connection from "{request.remote}" failed due to missing User-Id header.')
97-
await websocket.close(code=4000, message=b'Missing "User-Id" header.')
95+
logger.error(f'<{client_name}> - Websocket connection failed due to missing \'User-Id\' header.')
96+
await websocket.close(code=4000, message=b'Missing \'User-Id\' header.')
9897
return websocket
9998

10099
# Authorization
101100
authorization = request.headers.get('Authorization')
102101

103102
if CONFIG['SERVER']['password'] != authorization:
104-
logger.error(f'Websocket connection from <{client_name}> failed due to mismatched Authorization header: {authorization}')
103+
logger.error(f'<{client_name}> - Websocket connection failed due to mismatched \'Authorization\' header: {authorization}')
105104
await websocket.close(code=4001, message=b'Authorization failed.')
106105
return websocket
107106

@@ -111,38 +110,35 @@ async def websocket_handler(self, request: aiohttp.web.Request) -> aiohttp.web.W
111110
websocket['user_id'] = user_id
112111
websocket['players'] = {}
113112

114-
logger.info(f'Websocket connection from <{client_name}> established.')
113+
logger.info(f'<{client_name}> - Websocket connection established.')
115114

116115
# Handle incoming messages
117116
async for message in websocket: # type: aiohttp.WSMessage
118117

119118
try:
120119
payload = message.json()
121-
logger.debug(f'Received payload from <{client_name}>.\nPayload: {payload}')
122120
except Exception:
123-
logger.error(f'Received payload with invalid JSON format from <{client_name}>.\nPayload: {message.data}')
121+
logger.error(f'<{client_name}> - Received payload with invalid JSON format.\nPayload: {message.data}')
124122
continue
125123

126124
if 'op' not in payload:
127-
logger.error(f'Received payload with missing "op" key from <{client_name}>. Discarding.')
125+
logger.error(f'<{client_name}> - Received payload with missing \'op\' key.\nPayload: {payload}')
128126
continue
129-
130127
if 'd' not in payload:
131-
logger.error(f'Received payload with missing "d" key from <{client_name}>. Discarding.')
128+
logger.error(f'<{client_name}> - Received payload with missing \'d\' key.\nPayload: {payload}')
132129
continue
133130

134-
if not (guild_id := payload['d'].get('guild_id')):
135-
logger.error(f'Received payload with missing "guild_id" data key from <{client_name}>. Discarding.')
131+
if not (guild_id := payload['d'].get('guild_id', None)):
132+
logger.error(f'<{client_name}> - Received payload with missing \'guild_id\' data key. Payload: {payload}')
136133
continue
137134

138-
player: Player | None = websocket['players'].get(guild_id)
139-
if not player:
135+
if not (player := websocket['players'].get(guild_id)): # type: Player | None
140136
player = Player(self, websocket, guild_id, user_id)
141137
websocket['players'][guild_id] = player
142138

143139
await player.handle_payload(payload)
144140

145-
logger.info(f'Websocket connection from <{client_name}> closed.')
141+
logger.info(f'<{client_name}> - Websocket connection closed.')
146142
return websocket
147143

148144
# Rest handlers
@@ -196,7 +192,7 @@ async def _ytdl_search(self, query: str, internal: bool) -> Any:
196192
async def _get_playback_url(self, url: str) -> str:
197193

198194
search = await self._ytdl_search(url, internal=True)
199-
return search["url"]
195+
return search['url']
200196

201197
async def _get_tracks(self, query: str) -> list[dict[str, Any]]:
202198

@@ -230,15 +226,15 @@ async def search_tracks(self, request: aiohttp.web.Request) -> aiohttp.web.Respo
230226

231227
query = request.query.get('query')
232228
if not query:
233-
return aiohttp.web.json_response({'error': 'Missing "query" query parameter.'}, status=400)
229+
return aiohttp.web.json_response({'error': 'Missing \'query\' query parameter.'}, status=400)
234230

235231
source = request.query.get('source', 'youtube')
236232
if (url := yarl.URL(query)) and url.host and url.scheme:
237233
source = 'none'
238234

239235
prefix = self._SOURCE_MAPPING.get(source)
240236
if prefix is None:
241-
return aiohttp.web.json_response({'error': 'Invalid "source" query parameter.'}, status=400)
237+
return aiohttp.web.json_response({'error': 'Invalid \'source\' query parameter.'}, status=400)
242238

243239
tracks = await self._get_tracks(f'{prefix}{query}')
244240

swish/player.py

Lines changed: 85 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@
1515
You should have received a copy of the GNU Affero General Public License
1616
along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
"""
18+
1819
from __future__ import annotations
1920

2021
import asyncio
2122
import logging
2223
from collections.abc import Awaitable, Callable
23-
from typing import TYPE_CHECKING, Any
24+
from typing import Any, TYPE_CHECKING
2425

2526
import aiohttp
2627
import aiohttp.web
2728
from discord.backoff import ExponentialBackoff
2829
from discord.ext.native_voice import _native_voice
2930

31+
3032
if TYPE_CHECKING:
3133
from .app import App
3234

@@ -50,69 +52,123 @@ def __init__(
5052
self._user_id: str = user_id
5153

5254
self._connector: _native_voice.VoiceConnector = _native_voice.VoiceConnector()
55+
self._connector.user_id = int(user_id)
56+
5357
self._connection: _native_voice.VoiceConnection | None = None
5458
self._runner: asyncio.Task[None] | None = None
5559

56-
self._connector.user_id = int(user_id)
60+
self._endpoint: str | None = None
5761

58-
self.OP_HANDLERS: dict[str, Callable[[dict[str, Any]], Awaitable[None]]] = {
59-
'voice_update': self._voice_update,
60-
'destroy': self._destroy,
61-
'play': self._play,
62-
'stop': self._stop,
62+
self._OP_HANDLERS: dict[str, Callable[[dict[str, Any]], Awaitable[None]]] = {
63+
'voice_update': self._voice_update,
64+
'destroy': self._destroy,
65+
'play': self._play,
66+
'stop': self._stop,
6367
'set_pause_state': self._set_pause_state,
64-
'set_position': self._set_position,
65-
'set_filter': self._set_filter,
68+
'set_position': self._set_position,
69+
'set_filter': self._set_filter,
70+
'debug': self._debug,
6671
}
6772

73+
self._LOG_PREFIX: str = f'<{self._websocket["client_name"]}> - Player \'{self._guild_id}\''
74+
75+
self._NO_CONNECTION_MESSAGE: Callable[[str], str] = (
76+
lambda op: f'{self._LOG_PREFIX} attempted \'{op}\' op while internal connection is down.'
77+
)
78+
self._MISSING_KEY_MESSAGE: Callable[[str, str], str] = (
79+
lambda op, key: f'{self._LOG_PREFIX} received \'{op}\' op with missing \'{key}\' key.'
80+
)
81+
6882
# websocket op handlers
6983

7084
async def handle_payload(self, payload: dict[str, Any]) -> None:
7185

72-
handler = self.OP_HANDLERS.get(payload['op'])
73-
if not handler:
74-
logger.error(f'Received payload with unknown "op" key from <{self._websocket["client_name"]}>. Discarding.')
86+
op = payload['op']
87+
88+
if not (handler := self._OP_HANDLERS.get(op)):
89+
logger.error(f'{self._LOG_PREFIX} received payload with unknown \'op\' key.\nPayload: {payload}')
7590
return
7691

92+
logger.debug(f'{self._LOG_PREFIX} received payload with \'{op}\' op.\nPayload: {payload}')
7793
await handler(payload['d'])
7894

7995
async def _voice_update(self, data: dict[str, Any]) -> None:
8096

8197
self._connector.session_id = data['session_id']
82-
token = data['token']
83-
guild_id = data['guild_id']
8498

85-
if (endpoint := data.get('endpoint')) is None:
99+
if not (endpoint := data.get('endpoint')):
86100
return
87101

88102
endpoint, _, _ = endpoint.rpartition(':')
89103
endpoint = endpoint.removeprefix('wss://')
104+
self._endpoint = endpoint
90105

91-
self._connector.update_socket(token, guild_id, endpoint)
106+
self._connector.update_socket(
107+
data['token'],
108+
data['guild_id'],
109+
endpoint
110+
)
92111
await self._connect()
93112

94-
async def _destroy(self, data: dict[str, Any]) -> None:
95-
raise NotImplementedError
113+
async def _destroy(self, _: dict[str, Any]) -> None:
114+
115+
await self._disconnect()
116+
logger.info(f'{self._LOG_PREFIX} has been disconnected.')
96117

97118
async def _play(self, data: dict[str, Any]) -> None:
98119

99-
info = self._app._decode_track_id(data['track_id'])
120+
if not self._connection:
121+
logger.error(self._NO_CONNECTION_MESSAGE('play'))
122+
return
123+
124+
if not (track_id := data.get('track_id')):
125+
logger.error(self._MISSING_KEY_MESSAGE('play', 'track_id'))
126+
return
127+
128+
info = self._app._decode_track_id(track_id)
100129
url = await self._app._get_playback_url(info['url'])
101130

102-
if self._connection:
103-
self._connection.play(url)
131+
self._connection.play(url)
132+
logger.info(f'{self._LOG_PREFIX} started playing track \'{info["title"]}\' by \'{info["author"]}\'.')
104133

105-
async def _stop(self, data: dict[str, Any]) -> None:
106-
raise NotImplementedError
134+
async def _stop(self, _: dict[str, Any]) -> None:
135+
136+
if not self._connection:
137+
logger.error(self._NO_CONNECTION_MESSAGE('stop'))
138+
return
139+
140+
self._connection.stop()
141+
logger.info(f'{self._LOG_PREFIX} stopped the current track.')
107142

108143
async def _set_pause_state(self, data: dict[str, Any]) -> None:
109-
raise NotImplementedError
144+
145+
if not self._connection:
146+
logger.error(self._NO_CONNECTION_MESSAGE('set_pause_state'))
147+
return
148+
if not (state := data.get('state')):
149+
logger.error(self._MISSING_KEY_MESSAGE('set_pause_state', 'state'))
150+
return
151+
152+
self._connection.pause() if state else self._connection.resume()
153+
logger.info(f'{self._LOG_PREFIX} set its paused state to \'{state}\'.')
110154

111155
async def _set_position(self, data: dict[str, Any]) -> None:
112-
raise NotImplementedError
113156

114-
async def _set_filter(self, data: dict[str, Any]) -> None:
115-
raise NotImplementedError
157+
if not self._connection:
158+
logger.error(self._NO_CONNECTION_MESSAGE('set_position'))
159+
return
160+
if not (position := data.get('position')):
161+
logger.error(self._MISSING_KEY_MESSAGE('set_position', 'position'))
162+
return
163+
164+
# TODO: implement
165+
logger.info(f'{self._LOG_PREFIX} set its position to \'{position}\'.')
166+
167+
async def _set_filter(self, _: dict[str, Any]) -> None:
168+
logger.error(f'{self._LOG_PREFIX} received \'set_filter\' op which is not yet implemented.')
169+
170+
async def _debug(self, _: dict[str, Any]) -> None:
171+
print(self._debug_info())
116172

117173
# internal connection handlers
118174

@@ -126,6 +182,7 @@ async def _connect(self) -> None:
126182
self._runner = loop.create_task(self._reconnect_handler())
127183

128184
self._websocket['players'][self._guild_id] = self
185+
logger.info(f'{self._LOG_PREFIX} connected to internal voice server \'{self._endpoint}\'.')
129186

130187
async def _reconnect_handler(self) -> None:
131188

@@ -169,6 +226,7 @@ async def _disconnect(self) -> None:
169226
self._connection = None
170227

171228
del self._websocket['players'][self._guild_id]
229+
logger.info(f'{self._LOG_PREFIX} disconnected from internal voice server \'{self._endpoint}\'.')
172230

173231
# utility
174232

0 commit comments

Comments
 (0)