21
21
#from nowplaying.exceptions import PluginVerifyError
22
22
from nowplaying .inputs import InputPlugin
23
23
24
- METADATA = {}
25
-
26
24
METADATALIST = ['artist' , 'title' , 'album' , 'key' , 'filename' , 'bpm' ]
27
25
28
26
PLAYLIST = ['name' , 'filename' ]
31
29
class IcecastProtocol (asyncio .Protocol ):
32
30
''' a terrible implementation of the Icecast SOURCE protocol '''
33
31
34
- def __init__ (self ):
32
+ def __init__ (self , metadata_callback = None ):
35
33
self .streaming = False
36
34
self .previous_page = b''
35
+ self .metadata_callback = metadata_callback
36
+ self ._current_metadata = {}
37
37
38
38
def connection_made (self , transport ):
39
39
''' initial connection gives us a transport to use '''
@@ -89,29 +89,46 @@ def _parse_page(self, dataio):
89
89
self .previous_page = b''
90
90
header_data = dataio .read (27 )
91
91
92
- @staticmethod
93
- def _query_parse (data ):
92
+ def _query_parse (self , data ):
94
93
''' try to parse the query '''
95
- global METADATA # pylint: disable=global-statement
96
94
logging .debug ('Processing updinfo' )
97
95
98
- METADATA = {}
99
- text = data .decode ('utf-8' ).replace ('GET ' , 'http://localhost' ).split ()[0 ]
100
- url = urllib .parse .urlparse (text )
96
+ metadata = {}
97
+ try :
98
+ text = data .decode ('utf-8' ).replace ('GET ' , 'http://localhost' ).split ()[0 ]
99
+ url = urllib .parse .urlparse (text )
100
+ except UnicodeDecodeError :
101
+ logging .warning ('Failed to decode icecast query data as UTF-8' )
102
+ return
103
+ except (IndexError , ValueError ) as error :
104
+ logging .warning ('Failed to parse icecast query URL: %s' , error )
105
+ return
101
106
if url .path == '/admin/metadata' :
102
- query = urllib .parse .parse_qs (url .query )
107
+ query = urllib .parse .parse_qs (url .query , keep_blank_values = True )
103
108
if query .get ('mode' ) == ['updinfo' ]:
104
109
if query .get ('artist' ):
105
- METADATA ['artist' ] = query ['artist' ][0 ]
110
+ metadata ['artist' ] = query ['artist' ][0 ]
106
111
if query .get ('title' ):
107
- METADATA ['title' ] = query ['title' ][0 ]
108
- if query .get ('song' ):
109
- METADATA ['title' ], METADATA ['artist' ] = query ['song' ][0 ].split ('-' )
110
-
111
- @staticmethod
112
- def _parse_vorbis_comment (fh ): # pylint: disable=invalid-name
112
+ metadata ['title' ] = query ['title' ][0 ]
113
+ if 'song' in query :
114
+ song_text = query ['song' ][0 ].strip ()
115
+ if ' - ' not in song_text :
116
+ # No separator found, treat entire string as title
117
+ metadata ['title' ] = song_text
118
+ else :
119
+ # Split on first occurrence of ' - ' (with spaces)
120
+ # This handles cases like "Artist - Song - Remix" correctly
121
+ artist , title = song_text .split (' - ' , 1 )
122
+ metadata ['artist' ] = artist .strip ()
123
+ metadata ['title' ] = title .strip ()
124
+
125
+ # Update instance metadata and notify callback
126
+ self ._current_metadata .update (metadata )
127
+ if self .metadata_callback :
128
+ self .metadata_callback (self ._current_metadata .copy ())
129
+
130
+ def _parse_vorbis_comment (self , fh ): # pylint: disable=invalid-name
113
131
''' from tinytag, with slight modifications, pull out metadata '''
114
- global METADATA # pylint: disable=global-statement
115
132
comment_type_to_attr_mapping = {
116
133
'album' : 'album' ,
117
134
'albumartist' : 'albumartist' ,
@@ -127,7 +144,7 @@ def _parse_vorbis_comment(fh): # pylint: disable=invalid-name
127
144
}
128
145
129
146
logging .debug ('Processing vorbis comment' )
130
- METADATA = {}
147
+ metadata = {}
131
148
132
149
vendor_length = struct .unpack ('I' , fh .read (4 ))[0 ]
133
150
fh .seek (vendor_length , os .SEEK_CUR ) # jump over vendor
@@ -141,7 +158,12 @@ def _parse_vorbis_comment(fh): # pylint: disable=invalid-name
141
158
if '=' in keyvalpair :
142
159
key , value = keyvalpair .split ('=' , 1 )
143
160
if fieldname := comment_type_to_attr_mapping .get (key .lower ()):
144
- METADATA [fieldname ] = value
161
+ metadata [fieldname ] = value
162
+
163
+ # Update instance metadata and notify callback
164
+ self ._current_metadata .update (metadata )
165
+ if self .metadata_callback :
166
+ self .metadata_callback (self ._current_metadata .copy ())
145
167
146
168
147
169
class Plugin (InputPlugin ):
@@ -154,6 +176,11 @@ def __init__(self, config=None, qsettings=None):
154
176
self .server = None
155
177
self .mode = None
156
178
self .lastmetadata = {}
179
+ self ._current_metadata = {}
180
+
181
+ def _metadata_callback (self , metadata ):
182
+ ''' Callback to receive metadata from the protocol '''
183
+ self ._current_metadata = metadata
157
184
158
185
def install (self ):
159
186
''' auto-install for Icecast '''
@@ -181,8 +208,8 @@ def desc_settingsui(self, qwidget):
181
208
#### Data feed methods
182
209
183
210
async def getplayingtrack (self ):
184
- ''' give back the metadata global '''
185
- return METADATA
211
+ ''' give back the current metadata '''
212
+ return self . _current_metadata
186
213
187
214
async def getrandomtrack (self , playlist ):
188
215
return None
@@ -196,7 +223,10 @@ async def start_port(self, port):
196
223
loop = asyncio .get_running_loop ()
197
224
logging .debug ('Launching Icecast on %s' , port )
198
225
try :
199
- self .server = await loop .create_server (IcecastProtocol , '' , port )
226
+ # Create protocol factory that passes the metadata callback
227
+ def protocol_factory ():
228
+ return IcecastProtocol (metadata_callback = self ._metadata_callback )
229
+ self .server = await loop .create_server (protocol_factory , '' , port )
200
230
except Exception as error : #pylint: disable=broad-except
201
231
logging .error ('Failed to launch icecast: %s' , error )
202
232
0 commit comments