Added arpeggios and cyclic zip operation
This commit is contained in:
8
examples/music21/music21_example.py
Normal file
8
examples/music21/music21_example.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from music21 import *
|
||||||
|
from sardine import *
|
||||||
|
from ziffers import *
|
||||||
|
|
||||||
|
|
||||||
|
s = to_music21('(i v vi vii^dim)@(q0 e2 s1 012)',time="4/4")
|
||||||
|
|
||||||
|
s.show('midi')
|
||||||
@ -2,12 +2,6 @@ from music21 import *
|
|||||||
from sardine import *
|
from sardine import *
|
||||||
from ziffers import *
|
from ziffers import *
|
||||||
|
|
||||||
|
|
||||||
s = to_music21('i v vi vii^dim',time="4/4")
|
|
||||||
|
|
||||||
s.show()
|
|
||||||
s.show('midi')
|
|
||||||
|
|
||||||
parsed = zparse('1 2 qr e 124')
|
parsed = zparse('1 2 qr e 124')
|
||||||
s2 = to_music21(parsed,time="4/4")
|
s2 = to_music21(parsed,time="4/4")
|
||||||
|
|
||||||
@ -6,7 +6,7 @@ import operator
|
|||||||
import random
|
import random
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from .defaults import DEFAULT_OPTIONS
|
from .defaults import DEFAULT_OPTIONS
|
||||||
from .common import repeat_text
|
from .common import repeat_text, cyclic_zip
|
||||||
from .scale import (
|
from .scale import (
|
||||||
note_from_pc,
|
note_from_pc,
|
||||||
midi_to_pitch_class,
|
midi_to_pitch_class,
|
||||||
@ -179,13 +179,13 @@ class Pitch(Event):
|
|||||||
if self.text is None:
|
if self.text is None:
|
||||||
self.text = str(self.pitch_class)
|
self.text = str(self.pitch_class)
|
||||||
self.update_note()
|
self.update_note()
|
||||||
#self._update_text()
|
# self._update_text()
|
||||||
|
|
||||||
def _update_text(self):
|
def _update_text(self):
|
||||||
if self.octave is not None:
|
if self.octave is not None:
|
||||||
self.text = repeat_text("^","_",self.octave) + self.text
|
self.text = repeat_text("^", "_", self.octave) + self.text
|
||||||
if self.modifier is not None:
|
if self.modifier is not None:
|
||||||
self.text = repeat_text("#","b",self.modifier) + self.text
|
self.text = repeat_text("#", "b", self.modifier) + self.text
|
||||||
|
|
||||||
def get_note(self):
|
def get_note(self):
|
||||||
"""Getter for note"""
|
"""Getter for note"""
|
||||||
@ -382,7 +382,9 @@ class Chord(Event):
|
|||||||
self.freqs = freqs
|
self.freqs = freqs
|
||||||
self.octaves = octaves
|
self.octaves = octaves
|
||||||
self.durations = durations
|
self.durations = durations
|
||||||
|
self.duration = durations[0]
|
||||||
self.beats = beats
|
self.beats = beats
|
||||||
|
self.text = "".join([val.text for val in self.pitch_classes])
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
@ -394,6 +396,7 @@ class RomanNumeral(Event):
|
|||||||
notes: list[int] = field(default=None, init=False)
|
notes: list[int] = field(default=None, init=False)
|
||||||
pitch_classes: list = field(default=None, init=False)
|
pitch_classes: list = field(default=None, init=False)
|
||||||
inversions: int = field(default=None)
|
inversions: int = field(default=None)
|
||||||
|
evaluated_chord: Chord = None
|
||||||
|
|
||||||
def set_notes(self, chord_notes: list[int]):
|
def set_notes(self, chord_notes: list[int]):
|
||||||
"""Set notes to roman numeral
|
"""Set notes to roman numeral
|
||||||
@ -414,6 +417,42 @@ class RomanNumeral(Event):
|
|||||||
for pitch in pitches:
|
for pitch in pitches:
|
||||||
self.pitch_classes.append(Pitch(**pitch))
|
self.pitch_classes.append(Pitch(**pitch))
|
||||||
|
|
||||||
|
def evaluate_chord(self, options: dict) -> Chord:
|
||||||
|
"""Create chord fom roman numeral"""
|
||||||
|
key = options["key"]
|
||||||
|
scale = options["scale"]
|
||||||
|
pitch_text = ""
|
||||||
|
pitch_classes = []
|
||||||
|
self.notes = chord_from_degree(
|
||||||
|
self.value, self.chord_type, options["scale"], options["key"]
|
||||||
|
)
|
||||||
|
for note in self.notes:
|
||||||
|
pitch_dict = midi_to_pitch_class(note, key, scale)
|
||||||
|
pitch_classes.append(
|
||||||
|
Pitch(
|
||||||
|
pitch_class=pitch_dict["pitch_class"],
|
||||||
|
note=note,
|
||||||
|
freq=midi_to_freq(note),
|
||||||
|
kwargs=(options | pitch_dict),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pitch_text += pitch_dict["text"]
|
||||||
|
|
||||||
|
chord = Chord(
|
||||||
|
text=pitch_text,
|
||||||
|
pitch_classes=pitch_classes,
|
||||||
|
duration=options["duration"],
|
||||||
|
notes=self.notes,
|
||||||
|
kwargs=options,
|
||||||
|
inversions=self.inversions,
|
||||||
|
)
|
||||||
|
|
||||||
|
chord.update_notes(options)
|
||||||
|
|
||||||
|
self.evaluated_chord = chord
|
||||||
|
|
||||||
|
return chord
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
class Function(Event):
|
class Function(Event):
|
||||||
@ -457,6 +496,9 @@ class Sequence(Meta):
|
|||||||
def __getitem__(self, index):
|
def __getitem__(self, index):
|
||||||
return self.values[index]
|
return self.values[index]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.values)
|
||||||
|
|
||||||
def update_local_options(self):
|
def update_local_options(self):
|
||||||
"""Update value attributes from dict"""
|
"""Update value attributes from dict"""
|
||||||
if self.local_options:
|
if self.local_options:
|
||||||
@ -558,7 +600,7 @@ class Sequence(Meta):
|
|||||||
item.update_options(options)
|
item.update_options(options)
|
||||||
item.update_notes(options)
|
item.update_notes(options)
|
||||||
elif isinstance(item, RomanNumeral):
|
elif isinstance(item, RomanNumeral):
|
||||||
item = _create_chord_from_roman(item, options)
|
item = item.evaluate_chord(options)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def _generative_repeat(tree: list, times: int, options: dict):
|
def _generative_repeat(tree: list, times: int, options: dict):
|
||||||
@ -637,40 +679,6 @@ class Sequence(Meta):
|
|||||||
)
|
)
|
||||||
return new_pitch
|
return new_pitch
|
||||||
|
|
||||||
def _create_chord_from_roman(current: RomanNumeral, options: dict) -> Chord:
|
|
||||||
"""Create chord fom roman numeral"""
|
|
||||||
key = options["key"]
|
|
||||||
scale = options["scale"]
|
|
||||||
pitch_text = ""
|
|
||||||
pitch_classes = []
|
|
||||||
current.notes = chord_from_degree(
|
|
||||||
current.value, current.chord_type, options["scale"], options["key"]
|
|
||||||
)
|
|
||||||
for note in current.notes:
|
|
||||||
pitch_dict = midi_to_pitch_class(note, key, scale)
|
|
||||||
pitch_classes.append(
|
|
||||||
Pitch(
|
|
||||||
pitch_class=pitch_dict["pitch_class"],
|
|
||||||
note=note,
|
|
||||||
freq=midi_to_freq(note),
|
|
||||||
kwargs=(options | pitch_dict),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pitch_text += pitch_dict["text"]
|
|
||||||
|
|
||||||
chord = Chord(
|
|
||||||
text=pitch_text,
|
|
||||||
pitch_classes=pitch_classes,
|
|
||||||
duration=options["duration"],
|
|
||||||
notes=current.notes,
|
|
||||||
kwargs=options,
|
|
||||||
inversions=current.inversions,
|
|
||||||
)
|
|
||||||
|
|
||||||
chord.update_notes(options)
|
|
||||||
|
|
||||||
return chord
|
|
||||||
|
|
||||||
# Start of the main function: Evaluate and flatten the Ziffers object tree
|
# Start of the main function: Evaluate and flatten the Ziffers object tree
|
||||||
values = self.evaluated_values if eval_tree else self.values
|
values = self.evaluated_values if eval_tree else self.values
|
||||||
for item in values:
|
for item in values:
|
||||||
@ -978,23 +986,31 @@ class ListOperation(Sequence):
|
|||||||
def evaluate(self, options=DEFAULT_OPTIONS.copy()):
|
def evaluate(self, options=DEFAULT_OPTIONS.copy()):
|
||||||
"""Evaluates the operation"""
|
"""Evaluates the operation"""
|
||||||
|
|
||||||
def filter_operation(input_list, options):
|
def _filter_whitespace(input_list):
|
||||||
flattened_list = []
|
for item in input_list:
|
||||||
|
if isinstance(item, Meta):
|
||||||
|
yield item
|
||||||
|
|
||||||
|
def _filter_operation(input_list, options):
|
||||||
|
"""Filter and evaluate values"""
|
||||||
|
flattened_list = []
|
||||||
for item in input_list:
|
for item in input_list:
|
||||||
if isinstance(item, (list, Sequence)):
|
if isinstance(item, (list, Sequence)):
|
||||||
if isinstance(item, ListOperation):
|
if isinstance(item, ListOperation):
|
||||||
flattened_list.extend(item.evaluated_values)
|
flattened_list.extend(item.evaluated_values)
|
||||||
else:
|
else:
|
||||||
flattened_list.append(filter_operation(item, options))
|
flattened_list.append(_filter_operation(item, options))
|
||||||
elif isinstance(item, Cyclic):
|
elif isinstance(item, Cyclic):
|
||||||
value = item.get_value()
|
value = item.get_value()
|
||||||
if isinstance(value, Sequence):
|
if isinstance(value, Sequence):
|
||||||
flattened_list.extend(filter_operation(value, options))
|
flattened_list.extend(_filter_operation(value, options))
|
||||||
elif isinstance(value, (Event, RandomInteger, Integer)):
|
elif isinstance(value, (Event, RandomInteger, Integer)):
|
||||||
flattened_list.append(value)
|
flattened_list.append(value)
|
||||||
elif isinstance(item, Modification):
|
elif isinstance(item, Modification):
|
||||||
options = options | item.as_options()
|
options = options | item.as_options()
|
||||||
|
elif isinstance(item, RomanNumeral):
|
||||||
|
item = item.evaluate_chord(options)
|
||||||
|
flattened_list.append(item)
|
||||||
elif isinstance(item, Range):
|
elif isinstance(item, Range):
|
||||||
flattened_list.extend(list(item.evaluate(options)))
|
flattened_list.extend(list(item.evaluate(options)))
|
||||||
elif isinstance(item, (Event, RandomInteger, Integer)):
|
elif isinstance(item, (Event, RandomInteger, Integer)):
|
||||||
@ -1006,9 +1022,123 @@ class ListOperation(Sequence):
|
|||||||
|
|
||||||
return flattened_list
|
return flattened_list
|
||||||
|
|
||||||
|
def _vertical_arpeggio(left, right, options):
|
||||||
|
"""Vertical arpeggio operation, eg. (135)@(q 1 2 021)"""
|
||||||
|
left = _filter_operation(left, options)
|
||||||
|
right = _filter_operation(right, options)
|
||||||
|
left = list(left.evaluate_tree(options))
|
||||||
|
right = list(right.evaluate_tree(options))
|
||||||
|
arp_items = []
|
||||||
|
|
||||||
|
for item in left:
|
||||||
|
for index in right:
|
||||||
|
pcs = item.pitch_classes
|
||||||
|
if isinstance(index, Pitch):
|
||||||
|
new_pitch = deepcopy(pcs[index.get_value(options) % len(pcs)])
|
||||||
|
new_pitch.duration = index.duration
|
||||||
|
arp_items.append(new_pitch)
|
||||||
|
else: # Should be a chord
|
||||||
|
new_pitches = []
|
||||||
|
for pitch in index.pitch_classes:
|
||||||
|
new_pitch = deepcopy(
|
||||||
|
pcs[pitch.get_value(options) % len(pcs)]
|
||||||
|
)
|
||||||
|
new_pitch.duration = pitch.duration
|
||||||
|
new_pitches.append(new_pitch)
|
||||||
|
new_chord = Chord(pitch_classes=new_pitches, kwargs=options)
|
||||||
|
new_chord.update_notes()
|
||||||
|
new_chord.text = "".join(
|
||||||
|
[val.text for val in new_chord.pitch_classes]
|
||||||
|
)
|
||||||
|
arp_items.append(new_chord)
|
||||||
|
|
||||||
|
return Sequence(values=arp_items)
|
||||||
|
|
||||||
|
def _horizontal_arpeggio(left, right, options):
|
||||||
|
"""Horizontal arpeggio operation, eg. (1 2 3 4)#(0 3 2 1)"""
|
||||||
|
left = _filter_operation(left, options)
|
||||||
|
right = _filter_operation(right, options)
|
||||||
|
left = list(left.evaluate_tree(options))
|
||||||
|
right = list(right.evaluate_tree(options))
|
||||||
|
arp_items = []
|
||||||
|
for index in right:
|
||||||
|
new_item = deepcopy(left[index.get_value(options) % len(left)])
|
||||||
|
arp_items.append(new_item)
|
||||||
|
return Sequence(values=arp_items)
|
||||||
|
|
||||||
|
def _cyclic_zip(left, right, options):
|
||||||
|
"""Cyclic zip operaiton, eg. (q e)<>(1 2 3)"""
|
||||||
|
left = list(_filter_whitespace(left))
|
||||||
|
right = list(_filter_whitespace(right))
|
||||||
|
result = Sequence(values=cyclic_zip(left, right))
|
||||||
|
return _filter_operation(result, options)
|
||||||
|
|
||||||
|
def _python_operations(left, right, options):
|
||||||
|
"""Python math operations"""
|
||||||
|
|
||||||
|
def __chord_operation(chord, pitch_y, yass, options):
|
||||||
|
"""Operation for single chords"""
|
||||||
|
new_pitches = []
|
||||||
|
pitch_y = pitch_y.get_value(options)
|
||||||
|
for pitch_x in chord.pitch_classes:
|
||||||
|
pitch_x = pitch_x.pitch_class
|
||||||
|
new_pitch = Pitch(
|
||||||
|
pitch_class=operation(
|
||||||
|
pitch_y if yass else pitch_x, pitch_x if yass else pitch_y
|
||||||
|
),
|
||||||
|
kwargs=options,
|
||||||
|
)
|
||||||
|
new_pitches.append(new_pitch)
|
||||||
|
new_chord = Chord(pitch_classes=new_pitches, kwargs=options)
|
||||||
|
new_chord.update_notes()
|
||||||
|
return new_chord
|
||||||
|
|
||||||
|
# _python_operation starts. Filter & evaluate items.
|
||||||
|
|
||||||
|
left = _filter_operation(left, options)
|
||||||
|
if isinstance(right, (Sequence, Cyclic)):
|
||||||
|
right = _filter_operation(right, options)
|
||||||
|
|
||||||
|
# Create product of items.
|
||||||
|
pairs = product(
|
||||||
|
(right.values if isinstance(right, Sequence) else [right]), left
|
||||||
|
)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for first, second in pairs:
|
||||||
|
if isinstance(first, Chord) and isinstance(second, Chord):
|
||||||
|
new_pitches = []
|
||||||
|
for pitch_x in first.pitch_classes:
|
||||||
|
for pitch_y in second.pitch_classes:
|
||||||
|
new_pitch = Pitch(
|
||||||
|
pitch_class=operation(
|
||||||
|
pitch_x.pitch_class, pitch_y.pitch_class
|
||||||
|
),
|
||||||
|
kwargs=options,
|
||||||
|
)
|
||||||
|
new_pitches.append(new_pitch)
|
||||||
|
new_chord = Chord(pitch_classes=new_pitches, kwargs=options)
|
||||||
|
new_chord.update_notes()
|
||||||
|
outcome = new_chord
|
||||||
|
elif isinstance(first, Chord):
|
||||||
|
outcome = __chord_operation(first, second, False, options)
|
||||||
|
elif isinstance(second, Chord):
|
||||||
|
outcome = __chord_operation(second, first, True, options)
|
||||||
|
else:
|
||||||
|
outcome = Pitch(
|
||||||
|
pitch_class=operation(
|
||||||
|
first.get_value(options), second.get_value(options)
|
||||||
|
),
|
||||||
|
kwargs=second.get_options(),
|
||||||
|
)
|
||||||
|
results.append(outcome)
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Start of the evaluate() function
|
||||||
|
|
||||||
operators = self.values[1::2] # Fetch every second operator element
|
operators = self.values[1::2] # Fetch every second operator element
|
||||||
values = self.values[::2] # Fetch every second list element
|
values = self.values[::2] # Fetch every second list element
|
||||||
values = filter_operation(values, options) # Filter out
|
# values = _filter_operation(values, options) # Filter out
|
||||||
if len(values) == 1:
|
if len(values) == 1:
|
||||||
return values[0] # If right hand doesnt contain anything sensible
|
return values[0] # If right hand doesnt contain anything sensible
|
||||||
left = values[0] # Start results with the first array
|
left = values[0] # Start results with the first array
|
||||||
@ -1016,16 +1146,15 @@ class ListOperation(Sequence):
|
|||||||
for i, operand in enumerate(operators):
|
for i, operand in enumerate(operators):
|
||||||
operation = operand.value
|
operation = operand.value
|
||||||
right = values[i + 1]
|
right = values[i + 1]
|
||||||
pairs = product(
|
if isinstance(operation, str):
|
||||||
(right.values if isinstance(right, Sequence) else [right]), left
|
if operation == "vertical":
|
||||||
)
|
left = _vertical_arpeggio(left, right, options)
|
||||||
left = [
|
elif operation == "horizontal":
|
||||||
Pitch(
|
left = _horizontal_arpeggio(left, right, options)
|
||||||
pitch_class=operation(x.get_value(options), y.get_value(options)),
|
if operation == "zip":
|
||||||
kwargs=y.get_options(),
|
left = _cyclic_zip(left, right, options)
|
||||||
)
|
else:
|
||||||
for (x, y) in pairs
|
left = _python_operations(left, right, options)
|
||||||
]
|
|
||||||
return left
|
return left
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
""" Common methods used in parsing """
|
""" Common methods used in parsing """
|
||||||
import re
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
def flatten(arr: list) -> list:
|
def flatten(arr: list) -> list:
|
||||||
"""Flattens array"""
|
"""Flattens array"""
|
||||||
@ -93,3 +93,20 @@ def euclidian_rhythm(pulses: int, length: int, rot: int = 0):
|
|||||||
bool_list = [_starts_descent(res_list, index) for index in range(length)]
|
bool_list = [_starts_descent(res_list, index) for index in range(length)]
|
||||||
|
|
||||||
return rotation(bool_list, rot)
|
return rotation(bool_list, rot)
|
||||||
|
|
||||||
|
|
||||||
|
def cyclic_zip(first: list, second: list) -> list:
|
||||||
|
"""Cyclic zip method
|
||||||
|
|
||||||
|
Args:
|
||||||
|
first (list): First list
|
||||||
|
second (list): Second list
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Cyclicly zipped list
|
||||||
|
"""
|
||||||
|
max_length = max(len(first), len(second))
|
||||||
|
result = []
|
||||||
|
for i in range(max_length):
|
||||||
|
result.append([first[i % len(first)], second[i % len(second)]])
|
||||||
|
return [deepcopy(item) for sublist in result for item in sublist]
|
||||||
|
|||||||
@ -66,6 +66,6 @@ class ZiffersMusic21(converter.subConverters.SubConverter):
|
|||||||
m_item = note.Rest(item.duration * 4)
|
m_item = note.Rest(item.duration * 4)
|
||||||
elif isinstance(item, Chord):
|
elif isinstance(item, Chord):
|
||||||
m_item = chord.Chord(item.notes)
|
m_item = chord.Chord(item.notes)
|
||||||
m_item.duration.quarterLength = item.durations * 4
|
m_item.duration.quarterLength = item.duration * 4
|
||||||
note_stream.append(m_item)
|
note_stream.append(m_item)
|
||||||
self.stream = note_stream.makeMeasures()
|
self.stream = note_stream.makeMeasures()
|
||||||
|
|||||||
@ -59,7 +59,10 @@ OPERATORS = MappingProxyType({
|
|||||||
"|": operator.or_,
|
"|": operator.or_,
|
||||||
"&": operator.and_,
|
"&": operator.and_,
|
||||||
"<<": operator.ilshift,
|
"<<": operator.ilshift,
|
||||||
">>": operator.irshift
|
">>": operator.irshift,
|
||||||
|
"@": "vertical",
|
||||||
|
"#": "horizontal",
|
||||||
|
"<>": "zip"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -301,7 +301,19 @@ def midi_to_pitch_class(note: int, key: str | int, scale: str) -> dict:
|
|||||||
|
|
||||||
def chord_from_degree(
|
def chord_from_degree(
|
||||||
degree: int, name: str, scale: str, root: str | int, num_octaves: int = 1
|
degree: int, name: str, scale: str, root: str | int, num_octaves: int = 1
|
||||||
):
|
) -> list[int]:
|
||||||
|
"""Generate chord from scale
|
||||||
|
|
||||||
|
Args:
|
||||||
|
degree (int): Chord degree
|
||||||
|
name (str): Chord type
|
||||||
|
scale (str): Scale name
|
||||||
|
root (str | int): Root for the chord
|
||||||
|
num_octaves (int, optional): Number of octaves. Defaults to 1.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[int]: Created chord as list of midi notes
|
||||||
|
"""
|
||||||
root = note_name_to_midi(root) if isinstance(root, str) else root
|
root = note_name_to_midi(root) if isinstance(root, str) else root
|
||||||
|
|
||||||
if name is None and scale.lower().capitalize() == "Chromatic":
|
if name is None and scale.lower().capitalize() == "Chromatic":
|
||||||
|
|||||||
@ -52,7 +52,8 @@
|
|||||||
// Right recursive list operation
|
// Right recursive list operation
|
||||||
list_op: list (operator right_op)+
|
list_op: list (operator right_op)+
|
||||||
right_op: list | number
|
right_op: list | number
|
||||||
operator: /([\+\-\*\/%\|\&]|<<|>>)/
|
//operator: "+" | "-" | "*" | "%" | "&" | "|" | "<<" | ">>" | "@" | "#"
|
||||||
|
operator: /([\+\-\*\/%\|\&]|<<|>>|@|#|<>)/
|
||||||
|
|
||||||
// Euclidean cycles
|
// Euclidean cycles
|
||||||
// TODO: Support randomization etc.
|
// TODO: Support randomization etc.
|
||||||
@ -72,7 +73,7 @@
|
|||||||
oct_change: escaped_octave WS
|
oct_change: escaped_octave WS
|
||||||
dur_change: (decimal | char_change)
|
dur_change: (decimal | char_change)
|
||||||
char_change: dchar_not_prefix+
|
char_change: dchar_not_prefix+
|
||||||
dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?=[ >])/
|
dchar_not_prefix: /([mklpdcwyhnqaefsxtgujzo](\.)*)(?=[ >)])/
|
||||||
|
|
||||||
// Generative rules
|
// Generative rules
|
||||||
random_integer: prefix* random_integer_re
|
random_integer: prefix* random_integer_re
|
||||||
|
|||||||
Reference in New Issue
Block a user