@@ -102,52 +102,107 @@ def parse_stream_spec(
102102 """
103103
104104 if isinstance (spec , str ):
105- out = {}
106- if file_index :
107- m = re .match (r"(\d+)(?::|$)" , spec )
108- if m :
109- out ["file_index" ] = int (m [1 ])
110- spec = spec [m .end () :]
105+
106+ out : StreamSpec = {}
107+ spec_parts = spec .split (":" )
108+ nspecs = len (spec_parts )
109+ i = 0 # current index
110+
111+ def get_int (s , name ):
112+ try :
113+ v = int (
114+ s ,
115+ (
116+ 10
117+ if s [0 ] != "0" and len (s ) > 1
118+ else 16 if s .startswith ("0x" ) or s .startswith ("0X" ) else 8
119+ ),
120+ )
121+ assert v >= 0
122+ except Exception as e :
123+ raise ValueError (f"Invalid { name } ({ s } )" ) from e
124+ return v
125+
126+ def get_id (i , name ):
127+
128+ try :
129+ s = spec_parts [i + 1 ]
130+ except IndexError as e :
131+ raise ValueError (f"Missing { name } " ) from e
111132 else :
112- raise ValueError ("Missing file index." )
113-
114- while len (spec ):
115- if spec .startswith ("p:" ):
116- _ , v , * r = spec .split (":" , 2 )
117- out ["program_id" ] = int (v )
118- spec = r [0 ] if len (r ) else ""
119- elif spec [0 ] in "vVadt" and (len (spec ) == 1 or spec [1 ] == ":" ):
120- out ["type" ], * r = spec .split (":" , 1 )
121- spec = r [0 ] if len (r ) else ""
133+ return get_int (s , name )
134+
135+ # add file index only if expected
136+ if file_index :
137+ out ["file_index" ] = get_int (spec_parts [0 ], "file index" )
138+ i += 1
139+
140+ # process the optional parts
141+ while i < nspecs :
142+ spec = spec_parts [i ]
143+ # optional specifiers first
144+ if spec in get_args (MediaType ):
145+ out ["media_type" ] = spec
146+ i += 1
147+ elif spec == "g" :
148+ i += 1
149+ spec = spec_parts [i ]
150+ if spec == "i" :
151+ out ["group_id" ] = get_id (i , "group_id" )
152+ i += 2
153+ elif spec .startswith ("#" ):
154+ out ["group_id" ] = get_int (spec [1 :], "group_id" )
155+ i += 1
156+ else :
157+ out ["group_index" ] = get_int (spec , "group index" )
158+ i += 1
159+ elif spec == "p" :
160+ out ["program_id" ] = get_id (i , "program_id" )
161+ i += 2
122162 else :
163+ # final primary specifier
164+ if spec .startswith ("#" ):
165+ out ["stream_id" ] = get_int (spec [1 :], "stream_id" )
166+ elif spec == "i" :
167+ out ["stream_id" ] = get_id (i , "stream_id" )
168+ i += 1
169+ elif spec == "u" :
170+ out ["usable" ] = True
171+ elif spec == "m" :
172+ try :
173+ key , * value = spec_parts [i + 1 :]
174+ assert len (value ) <= 1
175+ except (IndexError , AssertionError ) as e :
176+ raise ValueError (
177+ f"Invalid metadata tag specifier: { ':' .join (spec_parts [i :])} "
178+ ) from e
179+ else :
180+ i = nspecs - 1
181+ out ["tag" ] = (key , value [0 ]) if len (value ) else key
182+ else :
183+ try :
184+ out ["index" ] = get_int (spec , "stream_index" )
185+ except ValueError as e :
186+ raise ValueError (f"Unknown stream specifier: { spec } " ) from e
123187 break
124- if not spec :
125- return out
126-
127- try :
128- out ["index" ] = int (spec )
129- except :
130- m = re .match (
131- r"#(\d+)$|i\:(\d+)$|m\:(.+?)(?:\:(.+?))?$|(u)$|#(0x[\da-f]+)$|i\:(0x[\da-f]+)$" ,
132- spec ,
133- )
134- if not m :
135- raise ValueError ("Invalid stream specifier." )
136-
137- if m [1 ] or m [2 ]:
138- out ["pid" ] = int (m [1 ] or m [2 ])
139- elif m [3 ] is not None :
140- out ["tag" ] = m [3 ] if m [4 ] is None else (m [3 ], m [4 ])
141- elif m [5 ]:
142- out ["usable" ] = True
143- elif m [6 ] or m [7 ]:
144- out ["pid" ] = m [6 ] or m [7 ]
188+
189+ if i + 1 < nspecs :
190+ raise ValueError (f"Not all specifiers resolved: { ':' .join (spec_parts [i :])} " )
191+
145192 return out
146- else :
147- if file_index :
148- return {"file_index" : int (spec [0 ]), "index" : int (spec [1 ])}
149- else :
150- return {"index" : int (spec )}
193+
194+ if file_index :
195+ if not (
196+ isinstance (spec , Sequence )
197+ and len (spec ) == 2
198+ and all (isinstance (v , int ) and v >= 0 for v in spec )
199+ ):
200+ raise ValueError ("Invalid stream specifier" )
201+ return {"file_index" : int (spec [0 ]), "index" : int (spec [1 ])}
202+
203+ if not (isinstance (spec , int ) and spec >= 0 ):
204+ raise ValueError ("Invalid stream specifier" )
205+ return {"index" : int (spec )}
151206
152207
153208def is_stream_spec (spec , file_index : bool | None = None ) -> bool :
@@ -160,21 +215,23 @@ def is_stream_spec(spec, file_index: bool | None = None) -> bool:
160215 try :
161216 parse_stream_spec (spec , True if file_index is None else file_index )
162217 return True
163- except :
218+ except ValueError :
164219 if file_index is None :
165220 try :
166221 parse_stream_spec (spec , False )
167222 return True
168- except :
223+ except ValueError :
169224 pass
170225 return False
171226
172227
173228def stream_spec (
174229 index : int | None = None ,
175- type : MediaType | None = None ,
230+ media_type : MediaType | None = None ,
231+ group_index : int | None = None ,
232+ group_id : int | None = None ,
176233 program_id : int | None = None ,
177- pid : int | None = None ,
234+ stream_id : int | None = None ,
178235 tag : str | tuple [str , str ] | None = None ,
179236 usable : bool | None = None ,
180237 file_index : int | None = None ,
@@ -188,19 +245,22 @@ def stream_spec(
188245 streams as detected by libavformat except when a program ID is also
189246 specified. In this case it is based on the ordering of the streams in the
190247 program., defaults to None
191- :param type : One of following: ’v’ or ’V’ for video, ’a’ for audio, ’s’ for
248+ :param media_type : One of following: ’v’ or ’V’ for video, ’a’ for audio, ’s’ for
192249 subtitle, ’d’ for data, and ’t’ for attachments. ’v’ matches all video
193250 streams, ’V’ only matches video streams which are not attached pictures,
194251 video thumbnails or cover arts. If additional stream specifier is used, then
195252 it matches streams which both have this type and match the additional stream
196253 specifier. Otherwise, it matches all streams of the specified type, defaults
197254 to None
198- :type type: str, optional
255+ :param group_index: Matches streams which are in the group with this group index.
256+ Can be combined with other stream_specifiers, except for `group_index`.
257+ :param group_index: Matches streams which are in the group with this group id.
258+ Can be combined with other stream_specifiers, except for `group_id`.
199259 :param program_id: Selects streams which are in the program with this id. If
200260 additional_stream_specifier is used, then it matches streams which both are
201261 part of the program and match the additional_stream_specifier, defaults to
202262 None
203- :param pid : stream id given by the container (e.g. PID in MPEG-TS
263+ :param stream_id : stream id given by the container (e.g. PID in MPEG-TS
204264 container), defaults to None
205265 :param tag: metadata tag key having the specified value. If value is not
206266 given, matches streams that contain the given tag with any value, defaults
@@ -220,30 +280,31 @@ def stream_spec(
220280
221281 """
222282
223- # nothing specified
224- if all (
225- [ k is None for k in ( index , type , program_id , pid , tag , usable , file_index )]
226- ) :
227- return [] if no_join else ""
283+ if sum ( v is not None for v in ( index , stream_id , tag , usable )) > 1 :
284+ raise ValueError ( 'Only one of "index", "tag", or "usable" may be specified.' )
285+
286+ if sum ( v is not None for v in ( group_index , group_id )) > 1 :
287+ raise ValueError ( 'Only one of "group_index" or "group_id" may be specified.' )
228288
229289 spec = [] if file_index is None else [str (file_index )]
230290
231- if type is not None :
232- spec .append (
233- dict (video = "v" , audio = "a" , subtitle = "s" , data = "d" , attachment = "t" ).get (
234- type , type
235- )
236- )
291+ if media_type is not None :
292+ if media_type not in get_args (MediaType ):
293+ raise ValueError (f"Unknown { media_type = } ." )
294+ spec .append (media_type )
295+
296+ if group_index is not None :
297+ spec .append (f"g:{ group_index } " )
298+ elif group_id is not None :
299+ spec .append (f"g:#{ group_id } " )
237300
238301 if program_id is not None :
239302 spec .append (f"p:{ program_id } " )
240303
241- if sum ([k is not None for k in (index , pid , tag , usable )]) > 1 :
242- raise Exception ("Multiple mutually exclusive specifiers are given." )
243304 if index is not None :
244305 spec .append (str (index ))
245- elif pid is not None :
246- spec .append (f"#{ pid } " )
306+ elif stream_id is not None :
307+ spec .append (f"#{ stream_id } " )
247308 elif tag is not None :
248309 spec .append (f"m:{ tag } " if isinstance (tag , str ) else f"m:{ tag [0 ]} :{ tag [1 ]} " )
249310 elif usable is not None and usable :
0 commit comments