mirror of
https://github.com/iluvcapra/wavinfo.git
synced 2026-01-01 17:30:41 +00:00
Documentation
This commit is contained in:
98
README.md
98
README.md
@@ -53,104 +53,6 @@ The length of the file in frames (interleaved samples) and bytes is available, a
|
|||||||
>>> (48000, 2, 6, 24)
|
>>> (48000, 2, 6, 24)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Broadcast WAV Extension
|
|
||||||
|
|
||||||
A WAV file produced to Broadcast-WAV specifications will have the broadcast metadata extension,
|
|
||||||
which includes a 256-character free text descrption, creating entity identifier (usually the
|
|
||||||
recording application or equipment), the date and time of recording and a time reference for
|
|
||||||
timecode synchronization.
|
|
||||||
|
|
||||||
The `coding_history` is designed to contain a record of every conversion performed on the audio
|
|
||||||
file.
|
|
||||||
|
|
||||||
In this example (from a Sound Devices 702T) the bext metadata contains scene/take slating
|
|
||||||
information in the `description`. Here also the `originator_ref` is a serial number conforming
|
|
||||||
to EBU Rec 99.
|
|
||||||
|
|
||||||
If the bext metadata conforms to EBU 3285 v1, it will contain the WAV's 32 or 64 byte SMPTE
|
|
||||||
330M UMID. The 32-byte version of the UMID is usually just a random number, while the 64-byte
|
|
||||||
UMID will also have information on the recording date and time, recording equipment and entity,
|
|
||||||
and geolocation data.
|
|
||||||
|
|
||||||
If the bext metadata conforms to EBU 3285 v2, it will hold precomputed program loudness values
|
|
||||||
as described by EBU Rec 128.
|
|
||||||
|
|
||||||
```python
|
|
||||||
print(info.bext.description)
|
|
||||||
print("----------")
|
|
||||||
print("Originator:", info.bext.originator)
|
|
||||||
print("Originator Ref:", info.bext.originator_ref)
|
|
||||||
print("Originator Date:", info.bext.originator_date)
|
|
||||||
print("Originator Time:", info.bext.originator_time)
|
|
||||||
print("Time Reference:", info.bext.time_reference)
|
|
||||||
print(info.bext.coding_history)
|
|
||||||
```
|
|
||||||
|
|
||||||
sSPEED=023.976-ND
|
|
||||||
sTAKE=1
|
|
||||||
sUBITS=$12311801
|
|
||||||
sSWVER=2.67
|
|
||||||
sPROJECT=BMH
|
|
||||||
sSCENE=A101
|
|
||||||
sFILENAME=A101_1.WAV
|
|
||||||
sTAPE=18Y12M31
|
|
||||||
sTRK1=MKH516 A
|
|
||||||
sTRK2=Boom
|
|
||||||
sNOTE=
|
|
||||||
|
|
||||||
----------
|
|
||||||
Originator: Sound Dev: 702T S#GR1112089007
|
|
||||||
Originator Ref: USSDVGR1112089007124001008206301
|
|
||||||
Originator Date: 2018-12-31
|
|
||||||
Originator Time: 12:40:00
|
|
||||||
Time Reference: 2190940753
|
|
||||||
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### iXML Production Recorder Metadata
|
|
||||||
|
|
||||||
iXML allows an XML document to be embedded in a WAV file.
|
|
||||||
|
|
||||||
The iXML website recommends a schema for recorder information but
|
|
||||||
there is no official DTD and vendors mostly do their own thing, apart from
|
|
||||||
hitting a few key xpaths. iXML is used by most location/production recorders
|
|
||||||
to save slating information, timecode and sync points in a reliable way.
|
|
||||||
|
|
||||||
iXML is also used to link "families" of WAV files together, so WAV files
|
|
||||||
recorded simultaneously or contiguously can be related by a receiving client.
|
|
||||||
|
|
||||||
```python
|
|
||||||
print("iXML Project:", info.ixml.project)
|
|
||||||
print("iXML Scene:", info.ixml.scene)
|
|
||||||
print("iXML Take:", info.ixml.take)
|
|
||||||
print("iXML Tape:", info.ixml.tape)
|
|
||||||
print("iXML File Family Name:", info.ixml.family_name)
|
|
||||||
print("iXML File Family UID:", info.ixml.family_uid)
|
|
||||||
```
|
|
||||||
|
|
||||||
iXML Project: BMH
|
|
||||||
iXML Scene: A101
|
|
||||||
iXML Take: 1
|
|
||||||
iXML Tape: 18Y12M31
|
|
||||||
iXML File Family Name: None
|
|
||||||
iXML File Family UID: USSDVGR1112089007124001008206300
|
|
||||||
|
|
||||||
|
|
||||||
### INFO Metadata
|
|
||||||
|
|
||||||
INFO Metadata is a standard method for saving tagged text data in a WAV or AVI
|
|
||||||
file. INFO fields are often read by the file explorer and host OS, and used in
|
|
||||||
music library software.
|
|
||||||
|
|
||||||
```python
|
|
||||||
bullet_path = '../tests/test_files/BULLET Impact Plastic LCD TV Screen Shatter Debris 2x.wav'
|
|
||||||
|
|
||||||
bullet = WavInfoReader(bullet_path)
|
|
||||||
```
|
|
||||||
|
|
||||||
print("INFO Artist:", bullet.info.artist)
|
|
||||||
print("INFO Copyright:", bullet.info.copyright)
|
|
||||||
print("INFO Comment:", bullet.info.comment)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,6 @@
|
|||||||
Welcome to wavinfo's documentation!
|
Welcome to wavinfo's documentation!
|
||||||
===================================
|
===================================
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: Contents:
|
|
||||||
|
|
||||||
.. module:: wavinfo
|
.. module:: wavinfo
|
||||||
|
|
||||||
@@ -24,6 +21,17 @@ Welcome to wavinfo's documentation!
|
|||||||
.. autoclass:: wavinfo.wave_reader.WavDataDescriptor
|
.. autoclass:: wavinfo.wave_reader.WavDataDescriptor
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Notes:
|
||||||
|
|
||||||
|
metadata_scopes/bext.rst
|
||||||
|
metadata_scopes/ixml.rst
|
||||||
|
metadata_scopes/info.rst
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|||||||
65
docs/metadata_scopes/bext.rst
Normal file
65
docs/metadata_scopes/bext.rst
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
Broadcast WAV Extension
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. module:: wavinfo
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_bext_reader.WavBextReader
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
A WAV file produced to Broadcast-WAV specifications will have the broadcast metadata extension,
|
||||||
|
which includes a 256-character free text descrption, creating entity identifier (usually the
|
||||||
|
recording application or equipment), the date and time of recording and a time reference for
|
||||||
|
timecode synchronization.
|
||||||
|
|
||||||
|
The `coding_history` is designed to contain a record of every conversion performed on the audio
|
||||||
|
file.
|
||||||
|
|
||||||
|
In this example (from a Sound Devices 702T) the bext metadata contains scene/take slating
|
||||||
|
information in the `description`. Here also the `originator_ref` is a serial number conforming
|
||||||
|
to EBU Rec 99.
|
||||||
|
|
||||||
|
If the bext metadata conforms to EBU 3285 v1, it will contain the WAV's 32 or 64 byte SMPTE
|
||||||
|
330M UMID. The 32-byte version of the UMID is usually just a random number, while the 64-byte
|
||||||
|
UMID will also have information on the recording date and time, recording equipment and entity,
|
||||||
|
and geolocation data.
|
||||||
|
|
||||||
|
If the bext metadata conforms to EBU 3285 v2, it will hold precomputed program loudness values
|
||||||
|
as described by EBU Rec 128.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
print(info.bext.description)
|
||||||
|
print("----------")
|
||||||
|
print("Originator:", info.bext.originator)
|
||||||
|
print("Originator Ref:", info.bext.originator_ref)
|
||||||
|
print("Originator Date:", info.bext.originator_date)
|
||||||
|
print("Originator Time:", info.bext.originator_time)
|
||||||
|
print("Time Reference:", info.bext.time_reference)
|
||||||
|
print(info.bext.coding_history)
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
sSPEED=023.976-ND
|
||||||
|
sTAKE=1
|
||||||
|
sUBITS=$12311801
|
||||||
|
sSWVER=2.67
|
||||||
|
sPROJECT=BMH
|
||||||
|
sSCENE=A101
|
||||||
|
sFILENAME=A101_1.WAV
|
||||||
|
sTAPE=18Y12M31
|
||||||
|
sTRK1=MKH516 A
|
||||||
|
sTRK2=Boom
|
||||||
|
sNOTE=
|
||||||
|
|
||||||
|
----------
|
||||||
|
Originator: Sound Dev: 702T S#GR1112089007
|
||||||
|
Originator Ref: USSDVGR1112089007124001008206301
|
||||||
|
Originator Date: 2018-12-31
|
||||||
|
Originator Time: 12:40:00
|
||||||
|
Time Reference: 2190940753
|
||||||
|
A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch
|
||||||
32
docs/metadata_scopes/info.rst
Normal file
32
docs/metadata_scopes/info.rst
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
INFO Metadata
|
||||||
|
=============
|
||||||
|
|
||||||
|
|
||||||
|
.. module:: wavinfo
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_info_reader.WavInfoChunkReader
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
|
||||||
|
INFO Metadata is a standard method for saving tagged text data in a WAV or AVI
|
||||||
|
file. INFO fields are often read by the file explorer and host OS, and used in
|
||||||
|
music library software.
|
||||||
|
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
bullet_path = '../tests/test_files/BULLET Impact Plastic LCD TV Screen Shatter Debris 2x.wav'
|
||||||
|
|
||||||
|
bullet = WavInfoReader(bullet_path)
|
||||||
|
|
||||||
|
print("INFO Artist:", bullet.info.artist)
|
||||||
|
print("INFO Copyright:", bullet.info.copyright)
|
||||||
|
print("INFO Comment:", bullet.info.comment)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
44
docs/metadata_scopes/ixml.rst
Normal file
44
docs/metadata_scopes/ixml.rst
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
iXML Production Recorder Metadata
|
||||||
|
=================================
|
||||||
|
|
||||||
|
|
||||||
|
.. module:: wavinfo
|
||||||
|
|
||||||
|
.. autoclass:: wavinfo.wave_ixml_reader.WavIXMLFormat
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
iXML allows an XML document to be embedded in a WAV file.
|
||||||
|
|
||||||
|
The iXML website recommends a schema for recorder information but
|
||||||
|
there is no official DTD and vendors mostly do their own thing, apart from
|
||||||
|
hitting a few key xpaths. iXML is used by most location/production recorders
|
||||||
|
to save slating information, timecode and sync points in a reliable way.
|
||||||
|
|
||||||
|
iXML is also used to link "families" of WAV files together, so WAV files
|
||||||
|
recorded simultaneously or contiguously can be related by a receiving client.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
print("iXML Project:", info.ixml.project)
|
||||||
|
print("iXML Scene:", info.ixml.scene)
|
||||||
|
print("iXML Take:", info.ixml.take)
|
||||||
|
print("iXML Tape:", info.ixml.tape)
|
||||||
|
print("iXML File Family Name:", info.ixml.family_name)
|
||||||
|
print("iXML File Family UID:", info.ixml.family_uid)
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
iXML Project: BMH
|
||||||
|
iXML Scene: A101
|
||||||
|
iXML Take: 1
|
||||||
|
iXML Tape: 18Y12M31
|
||||||
|
iXML File Family Name: None
|
||||||
|
iXML File Family UID: USSDVGR1112089007124001008206300
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2,28 +2,12 @@ import struct
|
|||||||
|
|
||||||
class WavBextReader:
|
class WavBextReader:
|
||||||
def __init__(self,bext_data,encoding):
|
def __init__(self,bext_data,encoding):
|
||||||
# description[256]
|
"""
|
||||||
|
Read Broadcast-WAV extended metadata.
|
||||||
# originator[32]
|
:param best_data: The bytes-like data.
|
||||||
# originatorref[32]
|
"param encoding: The encoding to use when decoding the text fields of the
|
||||||
# originatordate[10] "YYYY:MM:DD"
|
BEXT metadata scope. According to EBU Rec 3285 this shall be ASCII.
|
||||||
# originatortime[8] "HH:MM:SS"
|
"""
|
||||||
# lowtimeref U32
|
|
||||||
# hightimeref U32
|
|
||||||
# version U16
|
|
||||||
#
|
|
||||||
# V1 field
|
|
||||||
# umid[64]
|
|
||||||
#
|
|
||||||
# V2 fields
|
|
||||||
# loudnessvalue S16 (in LUFS*100)
|
|
||||||
# loudnessrange S16 (in LUFS*100)
|
|
||||||
# maxtruepeak S16 (in dbTB*100)
|
|
||||||
# maxmomentaryloudness S16 (LUFS*100)
|
|
||||||
# maxshorttermloudness S16 (LUFS*100)
|
|
||||||
#
|
|
||||||
# reserved[180]
|
|
||||||
# codinghistory []
|
|
||||||
packstring = "<256s"+ "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
packstring = "<256s"+ "32s" + "32s" + "10s" + "8s" + "QH" + "64s" + "hhhhh" + "180s"
|
||||||
|
|
||||||
rest_starts = struct.calcsize(packstring)
|
rest_starts = struct.calcsize(packstring)
|
||||||
@@ -39,20 +23,38 @@ class WavBextReader:
|
|||||||
decoded = trimmed.decode(encoding)
|
decoded = trimmed.decode(encoding)
|
||||||
return decoded
|
return decoded
|
||||||
|
|
||||||
|
#: Description. A free-text field up to 256 characters long.
|
||||||
self.description = sanatize_bytes(unpacked[0])
|
self.description = sanatize_bytes(unpacked[0])
|
||||||
|
#: Originator. Usually the name of the encoding application, sometimes
|
||||||
|
#: a artist name.
|
||||||
self.originator = sanatize_bytes(unpacked[1])
|
self.originator = sanatize_bytes(unpacked[1])
|
||||||
|
#: A unique identifer for the file, a serial number.
|
||||||
self.originator_ref = sanatize_bytes(unpacked[2])
|
self.originator_ref = sanatize_bytes(unpacked[2])
|
||||||
|
#: Date of the recording, in the format YYY-MM-DD
|
||||||
self.originator_date = sanatize_bytes(unpacked[3])
|
self.originator_date = sanatize_bytes(unpacked[3])
|
||||||
|
#: Time of the recording, in the format HH:MM:SS.
|
||||||
self.originator_time = sanatize_bytes(unpacked[4])
|
self.originator_time = sanatize_bytes(unpacked[4])
|
||||||
|
#: The sample offset of the start of the file relative to an
|
||||||
|
#: epoch, usually midnight the day of the recording.
|
||||||
self.time_reference = unpacked[5]
|
self.time_reference = unpacked[5]
|
||||||
self.version = unpacked[6]
|
#: A variable-length text field containing a list of processes and
|
||||||
self.umid = None
|
#: and conversions performed on the file.
|
||||||
self.loudness_value = None
|
|
||||||
self.loudness_range = None
|
|
||||||
self.max_true_peak = None
|
|
||||||
self.max_momentary_loudness = None
|
|
||||||
self.max_shortterm_loudness = None
|
|
||||||
self.coding_history = sanatize_bytes(bext_data[rest_starts:])
|
self.coding_history = sanatize_bytes(bext_data[rest_starts:])
|
||||||
|
#: BEXT version.
|
||||||
|
self.version = unpacked[6]
|
||||||
|
#: SMPTE 330M UMID of this audio file, 64 bytes are allocated though the UMID
|
||||||
|
#: may only be 32 bytes long.
|
||||||
|
self.umid = None
|
||||||
|
#: EBU R128 Integrated loudness, in LUFS.
|
||||||
|
self.loudness_value = None
|
||||||
|
#: EBU R128 Loudness rante, in LUFS.
|
||||||
|
self.loudness_range = None
|
||||||
|
#: True peak level, in dBFS TP
|
||||||
|
self.max_true_peak = None
|
||||||
|
#: EBU R128 Maximum momentary loudness, in LUFS
|
||||||
|
self.max_momentary_loudness = None
|
||||||
|
#: EBU R128 Maximum short-term loudness, in LUFS.
|
||||||
|
self.max_shortterm_loudness = None
|
||||||
|
|
||||||
if self.version > 0:
|
if self.version > 0:
|
||||||
self.umid = unpacked[7]
|
self.umid = unpacked[7]
|
||||||
|
|||||||
@@ -14,18 +14,30 @@ class WavInfoChunkReader:
|
|||||||
|
|
||||||
self.info_chunk = next((chunk for chunk in list_chunks \
|
self.info_chunk = next((chunk for chunk in list_chunks \
|
||||||
if chunk.signature == b'INFO'), None)
|
if chunk.signature == b'INFO'), None)
|
||||||
|
|
||||||
|
#: 'ICOP' Copyright
|
||||||
self.copyright = self._get_field(f,b'ICOP')
|
self.copyright = self._get_field(f,b'ICOP')
|
||||||
|
#: 'IPRD' Product
|
||||||
self.product = self._get_field(f,b'IPRD')
|
self.product = self._get_field(f,b'IPRD')
|
||||||
|
#: 'IGNR' Genre
|
||||||
self.genre = self._get_field(f,b'IGNR')
|
self.genre = self._get_field(f,b'IGNR')
|
||||||
|
#: 'IART' Artist, composer, author
|
||||||
self.artist = self._get_field(f,b'IART')
|
self.artist = self._get_field(f,b'IART')
|
||||||
|
#: 'ICMT' Comment
|
||||||
self.comment = self._get_field(f,b'ICMT')
|
self.comment = self._get_field(f,b'ICMT')
|
||||||
|
#: 'ISFT' Software, encoding application
|
||||||
self.software = self._get_field(f,b'ISFT')
|
self.software = self._get_field(f,b'ISFT')
|
||||||
|
#: 'ICRD' Created date
|
||||||
self.created_date = self._get_field(f,b'ICRD')
|
self.created_date = self._get_field(f,b'ICRD')
|
||||||
|
#: 'IENG' Engineer
|
||||||
self.engineer = self._get_field(f,b'IENG')
|
self.engineer = self._get_field(f,b'IENG')
|
||||||
|
#: 'IKEY' Keywords, keyword list
|
||||||
self.keywords = self._get_field(f,b'IKEY')
|
self.keywords = self._get_field(f,b'IKEY')
|
||||||
|
#: 'INAM' Name, title
|
||||||
self.title = self._get_field(f,b'INAM')
|
self.title = self._get_field(f,b'INAM')
|
||||||
|
#: 'ISRC' Source
|
||||||
self.source = self._get_field(f,b'ISRC')
|
self.source = self._get_field(f,b'ISRC')
|
||||||
|
#: 'TAPE' Tape
|
||||||
self.tape = self._get_field(f,b'TAPE')
|
self.tape = self._get_field(f,b'TAPE')
|
||||||
|
|
||||||
|
|
||||||
@@ -43,6 +55,9 @@ class WavInfoChunkReader:
|
|||||||
|
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
A dictionary with all of the key/values read from the INFO scope.
|
||||||
|
"""
|
||||||
return {'copyright': self.copyright,
|
return {'copyright': self.copyright,
|
||||||
'product': self.product,
|
'product': self.product,
|
||||||
'genre': self.genre,
|
'genre': self.genre,
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import io
|
|||||||
|
|
||||||
class WavIXMLFormat:
|
class WavIXMLFormat:
|
||||||
"""
|
"""
|
||||||
iXML recorder metadata, as defined by iXML 2.0
|
iXML recorder metadata.
|
||||||
"""
|
"""
|
||||||
def __init__(self, xml):
|
def __init__(self, xml):
|
||||||
|
"""
|
||||||
|
Parse iXML.
|
||||||
|
:param xml: A bytes-like object containing the iXML payload.
|
||||||
|
"""
|
||||||
self.source = xml
|
self.source = xml
|
||||||
xmlBytes = io.BytesIO(xml)
|
xmlBytes = io.BytesIO(xml)
|
||||||
self.parsed = ET.parse(xmlBytes)
|
self.parsed = ET.parse(xmlBytes)
|
||||||
@@ -17,26 +21,45 @@ class WavIXMLFormat:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def project(self):
|
def project(self):
|
||||||
|
"""
|
||||||
|
The project/film name entered for the recording.
|
||||||
|
"""
|
||||||
return self._get_text_value("PROJECT")
|
return self._get_text_value("PROJECT")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scene(self):
|
def scene(self):
|
||||||
|
"""
|
||||||
|
Scene/slate.
|
||||||
|
"""
|
||||||
return self._get_text_value("SCENE")
|
return self._get_text_value("SCENE")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def take(self):
|
def take(self):
|
||||||
|
"""
|
||||||
|
Take number.
|
||||||
|
"""
|
||||||
return self._get_text_value("TAKE")
|
return self._get_text_value("TAKE")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tape(self):
|
def tape(self):
|
||||||
|
"""
|
||||||
|
Tape name.
|
||||||
|
"""
|
||||||
return self._get_text_value("TAPE")
|
return self._get_text_value("TAPE")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def family_uid(self):
|
def family_uid(self):
|
||||||
|
"""
|
||||||
|
The globally-unique ID for this file family. This may be in the format
|
||||||
|
of a GUID, or an EBU Rec 9 source identifier, or some other dumb number.
|
||||||
|
"""
|
||||||
return self._get_text_value("FILE_SET/FAMILY_UID")
|
return self._get_text_value("FILE_SET/FAMILY_UID")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def family_name(self):
|
def family_name(self):
|
||||||
|
"""
|
||||||
|
The name of this file's file family.
|
||||||
|
"""
|
||||||
return self._get_text_value("FILE_SET/FAMILY_NAME")
|
return self._get_text_value("FILE_SET/FAMILY_NAME")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user