Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ docs/crowdin.py
*.mp3
*.m4a
*.wav
*.mp4
*.ogg
*.pcm
*.png
*.jpg
Expand Down
3 changes: 1 addition & 2 deletions discord/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from .widget import *
from .object import *
from .reaction import *
from . import utils, opus, abc, ui
from . import utils, opus, abc, ui, sinks
from .enums import *
from .embeds import *
from .mentions import *
Expand All @@ -57,7 +57,6 @@
from .sticker import *
from .stage_instance import *
from .interactions import *
from .sink import *
from .components import *
from .threads import *
from .bot import *
Expand Down
15 changes: 0 additions & 15 deletions discord/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
'NoEntryPointError',
'ExtensionFailed',
'ExtensionNotFound',
'RecordingException',
)


Expand Down Expand Up @@ -270,20 +269,6 @@ def __init__(self, shard_id: Optional[int]):
)
super().__init__(msg % shard_id)

class RecordingException(ClientException):
"""Exception that's thrown when there is an error while trying to record
audio from a voice channel.

.. versionadded:: 2.0
"""
pass

class SinkException(ClientException):
"""Raised when a Sink error occurs.

.. versionadded:: 2.0
"""

class InteractionResponded(ClientException):
"""Exception that's raised when sending another interaction response using
:class:`InteractionResponse` when one has already been done before.
Expand Down
2 changes: 1 addition & 1 deletion discord/opus.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
import time

from .errors import DiscordException, InvalidArgument
from .sink import RawData
from .sinks import RawData

if TYPE_CHECKING:
T = TypeVar("T")
Expand Down
19 changes: 19 additions & 0 deletions discord/sinks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
discord.sinks
~~~~~~~~~~~~~

A place to store all officially given voice sinks.

:copyright: 2021-present Pycord Development
:license: MIT, see LICENSE for more details.
"""
from .core import *
from .errors import *
from .m4a import *
from .mka import *
from .mkv import *
from .mp3 import *
from .mp4 import *
from .ogg import *
from .pcm import *
from .wave import *
120 changes: 31 additions & 89 deletions discord/sink.py → discord/sinks/core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""
The MIT License (MIT)

Copyright (c) 2015-2021 Rapptz & (c) 2021-present Pycord-Development
Copyright (c) 2015-2021 Rapptz
Copyright (c) 2021-present Pycord Development

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
Expand All @@ -21,17 +22,13 @@
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import wave
import logging
import os
import struct
import sys
import threading
import time
import subprocess
import sys
import struct
from .errors import SinkException

_log = logging.getLogger(__name__)
from .errors import SinkException

__all__ = (
"Filters",
Expand All @@ -57,21 +54,22 @@
class Filters:
"""Filters for sink

.. versionadded:: 2.0
.. versionadded:: 2.1

Parameters
----------
interface: :meth:`Filters.interface`

container
Container of all Filters.
"""

def __init__(self, **kwargs):
self.filtered_users = kwargs.get("users", default_filters["users"])
self.seconds = kwargs.get("time", default_filters["time"])
self.max_size = kwargs.get("max_size", default_filters["max_size"])
self.finished = False

@staticmethod
def interface(func): # Contains all filters
def container(func): # Contains all filters
def _filter(self, data, user):
if not self.filtered_users or user in self.filtered_users:
return func(self, data, user)
Expand All @@ -87,14 +85,13 @@ def wait_and_stop(self):
time.sleep(self.seconds)
if self.finished:
return
self.vc.stop_listening()
self.vc.stop_recording()


class RawData:
"""Handles raw data from Discord so that it can be decrypted and decoded to be used.

.. versionadded:: 2.0

.. versionadded:: 2.1
"""

def __init__(self, data, client):
Expand All @@ -116,9 +113,7 @@ def __init__(self, data, client):

class AudioData:
"""Handles data that's been completely decrypted and decoded and is ready to be saved to file.

.. versionadded:: 2.0

.. versionadded:: 2.1
Raises
------
ClientException
Expand Down Expand Up @@ -158,40 +153,38 @@ def on_format(self, encoding):
class Sink(Filters):
"""A Sink "stores" all the audio data.

.. versionadded:: 2.0
Can be subclassed for extra customizablilty,

.. warning::
It is although recommended you use,
the officially provided sink classes
like :class:`~discord.sinks.WaveSink`

just replace the following like so: ::
vc.start_recording(
MySubClassedSink(),
finished_callback,
ctx.channel,
)
.. versionadded:: 2.1

Parameters
----------
encoding: :class:`string`
The encoding to use. Valid types include wav, mp3, and pcm (even though it's not an actual encoding).
output_path: :class:`string`
A path to where the audio files should be output.

Raises
------
ClientException
An invalid encoding type was specified.
Audio may only be formatted after recording is finished.
"""

valid_encodings = [
"wav",
"mp3",
"pcm",
]

def __init__(self, *, encoding="wav", output_path="", filters=None):
def __init__(self, *, output_path="", filters=None):
if filters is None:
filters = default_filters
self.filters = filters
Filters.__init__(self, **self.filters)

encoding = encoding.lower()

if encoding not in self.valid_encodings:
raise SinkException("An invalid encoding type was specified.")

self.encoding = encoding
self.file_path = output_path
self.vc = None
self.audio_data = {}
Expand All @@ -200,7 +193,7 @@ def init(self, vc): # called under listen
self.vc = vc
super().init()

@Filters.interface
@Filters.container
def write(self, data, user):
if user not in self.audio_data:
ssrc = self.vc.get_ssrc(user)
Expand All @@ -214,55 +207,4 @@ def cleanup(self):
self.finished = True
for file in self.audio_data.values():
file.cleanup()
self.format_audio(file)

def format_audio(self, audio):
if self.vc.recording:
raise SinkException(
"Audio may only be formatted after recording is finished."
)
if self.encoding == "pcm":
return
if self.encoding == "mp3":
mp3_file = audio.file.split(".")[0] + ".mp3"
args = [
"ffmpeg",
"-f",
"s16le",
"-ar",
"48000",
"-ac",
"2",
"-i",
audio.file,
mp3_file,
]
process = None
if os.path.exists(mp3_file):
os.remove(
mp3_file
) # process will get stuck asking whether or not to overwrite, if file already exists.
try:
process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW)
except FileNotFoundError:
raise SinkException("ffmpeg was not found.") from None
except subprocess.SubprocessError as exc:
raise SinkException(
"Popen failed: {0.__class__.__name__}: {0}".format(exc)
) from exc
process.wait()
elif self.encoding == "wav":
with open(audio.file, "rb") as pcm:
data = pcm.read()
pcm.close()

wav_file = audio.file.split(".")[0] + ".wav"
with wave.open(wav_file, "wb") as f:
f.setnchannels(self.vc.decoder.CHANNELS)
f.setsampwidth(self.vc.decoder.SAMPLE_SIZE // self.vc.decoder.CHANNELS)
f.setframerate(self.vc.decoder.SAMPLING_RATE)
f.writeframes(data)
f.close()

os.remove(audio.file)
audio.on_format(self.encoding)
self.format_audio(file)
88 changes: 88 additions & 0 deletions discord/sinks/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""
The MIT License (MIT)

Copyright (c) 2021-present Pycord Development

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from discord.errors import DiscordException


class SinkException(DiscordException):
"""Raised when a Sink error occurs.

.. versionadded:: 2.1
"""


class RecordingException(SinkException):
"""Exception that's thrown when there is an error while trying to record
audio from a voice channel.

.. versionadded:: 2.1
"""

pass


class MP3SinkError(SinkException):
"""Exception thrown when a exception occurs with :class:`MP3Sink`

.. versionadded:: 2.1
"""


class MP4SinkError(SinkException):
"""Exception thrown when a exception occurs with :class:`MP4Sink`

.. versionadded:: 2.1
"""


class OGGSinkError(SinkException):
"""Exception thrown when a exception occurs with :class:`OGGSink`

.. versionadded:: 2.1
"""


class MKVSinkError(SinkException):
"""Exception thrown when a exception occurs with :class:`MKVSink`

.. versionadded:: 2.1
"""


class WaveSinkError(SinkException):
"""Exception thrown when a exception occurs with :class:`WaveSink`

.. versionadded:: 2.1
"""

class M4ASinkError(SinkException):
"""Exception thrown when a exception occurs with :class:`M4ASink`

.. versionadded:: 2.1
"""

class MKASinkError(SinkException):
"""Exception thrown when a exception occurs with :class:`MKAsSink`

.. versionadded:: 2.1
"""
Loading