Repository: kijk2869/discodo (language: python)

Issue:
[Deprecated] Sanic variables
```
Setting variables on Sanic instances is deprecated and will be removed in version 21.9.

You should change your Sanic instance to use instance.ctx.ClientManagers instead.
```


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 1b77d315..acc5b701 100644
--- a/.gitignore
+++ b/.gitignore
@@ -128,3 +128,4 @@ dmypy.json
 # Pyre type checker
 .pyre/
 .vscode/settings.json
+bot.py
diff --git a/Dockerfile b/Dockerfile
index 0f41621f..762d536c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,9 +5,6 @@
 # Python 3.8
 FROM python:3.8
 
-# You can override this arg using --build-arg
-ARG discodoRepoURL=https://github.com/kijk2869/discodo
-
 # Open default discodo node port
 EXPOSE 8000
 
@@ -20,24 +17,20 @@ RUN apt-get update && \
     apt-get install --no-install-recommends -y pkg-config ffmpeg libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev libswscale-dev libavresample-dev libavfilter-dev && \
     apt-get clean && rm -rf /var/lib/apt/lists/*
 
-# install discodo from pypi
-RUN pip install --no-cache-dir --force-reinstall git+${discodoRepoURL} --no-binary av && \
-    echo "Discodo version: "$(python -m discodo --version)
-
-# Set workdir to /temp/discodo
-WORKDIR /temp/discodo
+# Set workdir to /opt/discodo
+WORKDIR /opt/discodo
 
-# Clone discodo repository
-RUN git clone ${discodoRepoURL}
+# Copy discodo repository
+COPY . .
 
-# Move example config.inc.json to /opt/discodo/config.json
-RUN mv discodo/example/remote/config.inc.json /opt/discodo/config.json
+# Move example config file
+RUN mv example/config.inc.json config.json
 
-# Set workdir to /opt/discodo
-WORKDIR /opt/discodo
+# install pypi dependencies
+RUN pip install --no-cache-dir --force-reinstall -r requirements.txt --no-binary av
 
-# Remove Cloned repository
-RUN rm -rf /temp/discodo
+# Echo discodo version
+RUN echo "Discodo version: "$(python -m discodo --version)
 
 # Run discodo with --config /opt/discodo/config.json option
 CMD [ "python", "-m", "discodo", "--config", "/opt/discodo/config.json" ]
diff --git a/README.md b/README.md
index bcda4db3..770553a8 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Discodo is an enhanced audio player for discord.
 
 ## Documentation
 
-**More information can be found [here](./docs).**
+**More information can be found [here](https://discodo.readthedocs.io/en/3.0/).**
 
 ## Installation
 
@@ -43,7 +43,7 @@ python -m discodo
 ### Client libraries
 
 * [discodo](https://github.com/kijk2869/discodo) (Python)
-* ~~[discodo.js](Under Developing) (Node.JS)~~
+* [discodo.js](https://github.com/kijk2869/discodo.js) (Node.JS)
 
 ## Supported sources
 
diff --git a/discodo/__init__.py b/discodo/__init__.py
index d2835bd0..ca2b51e6 100644
--- a/discodo/__init__.py
+++ b/discodo/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "2.3.14"
+__version__ = "3.0.0"
 
 import os
 
@@ -8,12 +8,12 @@
 from .config import Config
 from .enums import *
 from .errors import *
-from .event import DiscordEvent
+from .events import *
 from .extractor import extract
 from .manager import ClientManager
 from .natives import *
+from .planner import RoutePlanner
 from .player import Player
 from .source import *
-from .status import getStatus
 from .utils import *
-from .voice_client import VoiceClient
+from .voice_client import DiscordVoiceClient
diff --git a/discodo/__main__.py b/discodo/__main__.py
index 56cf0de5..2d85b369 100644
--- a/discodo/__main__.py
+++ b/discodo/__main__.py
@@ -5,65 +5,50 @@
 import os
 import sys
 
-import colorlog
 import psutil
+from rich.logging import RichHandler
 
 from . import __version__
 from .config import Config
+from .errors import OpusLoadError
 from .natives import opus
 
 log = logging.getLogger("discodo")
 
 
 if not opus.isLoaded() and not opus.loadDefaultOpus():
-    raise ValueError(
-        "Cannot load libopus, please install `libopus-dev` if you are using linux."
+    raise OpusLoadError(
+        "Cannot load libopus, please check your python architecture."
+        if sys.platform == "win32"
+        else "Cannot load libopus, please install `libopus-dev` if you are using linux."
     )
 
+accessHandler = RichHandler(rich_tracebacks=True)
+accessHandler.setFormatter(
+    logging.Formatter("%(name)s :\t%(request)s %(message)s %(status)d %(byte)d")
+)
 
-class loggingFilter(logging.Filter):
-    def __init__(self, level) -> None:
-        self.level = level
-
-    def filter(self, record) -> bool:
-        return record.levelno < self.level
-
-
-stdoutHandler = logging.StreamHandler(sys.stdout)
-stderrHandler = logging.StreamHandler(sys.stderr)
-stdoutHandler.addFilter(loggingFilter(logging.WARNING))
+genericHandler = RichHandler(rich_tracebacks=True)
+genericHandler.setFormatter(logging.Formatter("%(name)s :\t%(message)s"))
 
 
 def setLoggingLevel(level) -> None:
     for logger in [log, logging.getLogger("libav")]:
-        addLoggingHandler(logger)
-        logger.setLevel(logging.DEBUG)
-        stdoutHandler.setLevel(level)
-        stderrHandler.setLevel(max(level, logging.WARNING))
-
-
-ColoredFormatter = colorlog.ColoredFormatter(
-    "[%(asctime)s] [%(filename)s] [%(name)s:%(bold)s%(module)s%(reset)s] %(log_color)s[%(levelname)s]%(reset)s: %(message)s",
-    datefmt=None,
-    reset=True,
-    log_colors={
-        "DEBUG": "green",
-        "INFO": "cyan",
-        "WARNING": "yellow",
-        "ERROR": "red",
-        "CRITICAL": "red,bg_white",
-    },
-    secondary_log_colors={},
-    style="%",
-)
-stdoutHandler.setFormatter(ColoredFormatter)
-stderrHandler.setFormatter(ColoredFormatter)
+        logger.setLevel(level)
+        logger.addHandler(genericHandler)
+
+
+logging.getLogger("sanic").setLevel(logging.INFO)
 
+logging.getLogger("sanic.root").addHandler(genericHandler)
 
-def addLoggingHandler(logger) -> None:
-    logger.addHandler(stdoutHandler)
-    logger.addHandler(stderrHandler)
+errorLogger = logging.getLogger("sanic.error")
+errorLogger.propagate = True
+errorLogger.addHandler(genericHandler)
 
+accessLogger = logging.getLogger("sanic.access")
+accessLogger.propagate = True
+accessLogger.addHandler(accessHandler)
 
 parser = argparse.ArgumentParser("discodo")
 
@@ -160,9 +145,9 @@ def is_valid_json(parser, args):
 
 playerGroup.add_argument(
     "--default-volume",
-    type=int,
-    default=100,
-    help="player's default volume (default: 100)",
+    type=float,
+    default=1.0,
+    help="player's default volume (default: 1.0)",
 )
 playerGroup.add_argument(
     "--default-crossfade",
@@ -235,7 +220,7 @@ def is_valid_json(parser, args):
     Config.IPBLOCKS = args.ip
     Config.EXCLUDEIPS = args.exclude_ip
     Config.DEFAULT_AUTOPLAY = args.default_autoplay
-    Config.DEFAULT_VOLUME = round(args.default_volume / 100, 3)
+    Config.DEFAULT_VOLUME = args.default_volume
     Config.DEFAULT_CROSSFADE = args.default_crossfade
     Config.BUFFERLIMIT = args.bufferlimit
     Config.VCTIMEOUT = args.timeout
@@ -256,7 +241,16 @@ def is_valid_json(parser, args):
     if hasattr(psutil, "HIGH_PRIORITY_CLASS"):
         psutil.Process(os.getpid()).nice(psutil.HIGH_PRIORITY_CLASS)
 
-    loop = asyncio.get_event_loop()
+    try:
+        import uvloop
+    except ModuleNotFoundError:
+        loop = asyncio.get_event_loop()
+    else:
+        loop = uvloop.new_event_loop()
+
+        log.debug(f"uvloop {uvloop.__version__} detected, will use {loop}")
+
+    asyncio.set_event_loop(loop)
 
     if log.level == logging.DEBUG:
         loop.set_debug(True)
diff --git a/discodo/client/DPYClient.py b/discodo/client/DPYClient.py
index fae5cd6a..df1f4e03 100644
--- a/discodo/client/DPYClient.py
+++ b/discodo/client/DPYClient.py
@@ -1,32 +1,25 @@
 import asyncio
-import logging
-from itertools import chain
-from typing import Union
+import contextlib
+import itertools
 
 import discord
+import discord.ext
 
 from ..errors import NodeNotConnected, VoiceClientNotFound
-from ..utils import EventDispatcher
+from ..utils.eventDispatcher import EventDispatcher
 from .node import Node as OriginNode
 from .node import Nodes, launchLocalNode
-from .voice_client import VoiceClient
-
-log = logging.getLogger("discodo.client")
 
 
 class NodeClient(OriginNode):
-    def __init__(self, client, *args, **kwargs) -> None:
-        super().__init__(*args, **kwargs)
-        self.client = client
-
-    async def _resumed(self, Data: dict) -> None:
-        await super()._resumed(Data)
+    async def onResumed(self, Data):
+        await super().onResumed(Data)
 
         for guild_id, vc_data in Data["voice_clients"].items():
             guild = self.client.client.get_guild(int(guild_id))
             if "channel" in vc_data:
                 channel = guild.get_channel(vc_data["channel"])
-                self.loop.create_task(self.client.connect(channel))
+                self.loop.create_task(self.client.connect(channel, self))
             else:
                 self.loop.create_task(self.client.disconnect(guild))
 
@@ -36,31 +29,40 @@ async def close(self) -> None:
                 self.client.disconnect(self.client.client.get_guild(guildId))
             )
 
-        return super().close()
 
-    async def destroy(self, *args, **kwargs) -> None:
-        log.infmo(f"destroying Node {self.URL}")
-        await super().destroy(*args, **kwargs)
+class DPYClient:
+    r"""Represents a client connection that connects to Discodo.
+    This class will interact with Discodo nodes.
 
-        if self in self.client.Nodes:
-            self.client.Nodes.remove(self)
+    :param discord.Client class: The client of the bot with discord.py
 
+    :var list Nodes: The list of :py:class:`discodo.Node` that is registered.
+    :var EventDispatcher dispatcher: The event dispatcher that the client dispatches events.
+    :var asyncio.AbstractEventLoop loop: The event loop that the client uses for operation."""
 
-class DPYClient:
-    def __init__(self, client) -> None:
+    def __init__(self, client):
         self.client = client
         self.loop = client.loop or asyncio.get_event_loop()
 
         self.dispatcher = EventDispatcher()
-        self.event = self.dispatcher.event
+
+        self.GuildReservationMap = {}
 
         self.Nodes = Nodes()
-        self.__register_event()
+        self.__registerEvent()
 
     def __repr__(self) -> str:
         return f"<DPYClient Nodes={self.Nodes} voiceClients={len(self.voiceClients)}>"
 
-    def __register_event(self):
+    @property
+    def event(self):
+        r"""A decorator that registers an event to listen to.
+
+        :param str event: The event name to listen to."""
+
+        return self.dispatcher.event
+
+    def __registerEvent(self):
         if hasattr(self.client, "on_socket_response"):
             originFunc = self.client.on_socket_response
         else:
@@ -68,15 +70,32 @@ def __register_event(self):
 
         @self.client.event
         async def on_socket_response(*args, **kwargs):
-            self.loop.create_task(self.discord_socket_response(*args, **kwargs))
+            self.loop.create_task(self.discordDispatch(*args, **kwargs))
 
             if originFunc:
                 return await originFunc()
 
-    async def discord_socket_response(self, payload: dict) -> None:
+        if isinstance(self.client, discord.ext.commands.Bot):
+            originContextFunc = discord.ext.commands.Context.voice_client.fget
+
+            @discord.ext.commands.Context.voice_client.getter
+            def voice_client(ctx):
+                if ctx.bot == self.client:
+                    return self.getVC(ctx.guild, safe=True)
+
+                return originContextFunc(ctx)
+
+            discord.ext.commands.Context.voice_client = voice_client
+
+    async def discordDispatch(self, payload) -> None:
         if payload["t"] in ["VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"]:
             VC = self.getVC(payload["d"]["guild_id"], safe=True)
-            SelectNodes = [VC.Node] if VC else [self.getBestNode()]
+            SelectNodes = [
+                self.GuildReservationMap.get(
+                    int(payload["d"]["guild_id"]),
+                    (VC.Node if VC else self.getBestNode()),
+                )
+            ]
         else:
             SelectNodes = self.Nodes
 
@@ -91,80 +110,102 @@ async def discord_socket_response(self, payload: dict) -> None:
                 return_when="ALL_COMPLETED",
             )
 
-    def register_node(self, *args, **kwargs) -> None:
-        return self.loop.create_task(self._register_event(*args, **kwargs))
-
-    async def _register_event(self, *args, **kwargs) -> None:
-        await self.client.wait_until_ready()
-        kwargs["user_id"] = self.client.user.id
-
-        Node = NodeClient(self, *args, **kwargs)
-        await Node.connect()
-
-        log.info(f"registering Node {Node.host}:{Node.port}")
-
-        self.Nodes.append(Node)
-
-        Node.dispatcher.on("VC_DESTROYED", self._vc_destroyed)
-        Node.dispatcher.onAny(self._node_event)
-
-        return self
-
-    def register_local_node(self, *args, **kwargs) -> None:
-        return self.loop.create_task(self._register_local_event(*args, **kwargs))
-
-    async def _register_local_event(
-        self, *args, launchOptions: dict = {}, **kwargs
-    ) -> None:
-        await self.client.wait_until_ready()
+    def registerNode(
+        self,
+        host=None,
+        port=None,
+        password="hellodiscodo",
+        region=None,
+        launchOptions=None,
+    ):
+        r"""Creates a websocket connection of the node and register it on the client.
+
+        If the value ``host`` or ``port`` is ``None``, it will launch local node process.
+
+        :param Optional[str] host: The host of the node to connect to.
+        :param Optional[int] port: The port of the node to connect to.
+        :param Optional[str] password: The password of the node to connect to, defaults to ``hellodiscodo``.
+        :param Optional[str] region: The region of the node to connect to. This is like a annotation. It is not involved in the actual connection.
+        :param Optional[dict] launchOptions: The options to use when launching a local node
+
+        :return: The scheduled task to connect with the node
+        :rtype: asyncio.Task"""
+
+        if launchOptions is None:
+            launchOptions = {}
+
+        async def connectNode(self, host, port, password, region, launchOptions):
+            await self.client.wait_until_ready()
+
+            if not host or not port:
+                LocalNodeProc = await launchLocalNode(**launchOptions)
+
+                host = LocalNodeProc.HOST
+                port = LocalNodeProc.PORT
+                password = LocalNodeProc.PASSWORD
+
+            user_id = self.client.user.id
+            shard_id = (
+                self.client.shard_id
+                if isinstance(self.client, discord.Client)
+                else None
+            )
 
-        LocalNodeProc = await launchLocalNode(**launchOptions)
+            Node = NodeClient(self, host, port, user_id, shard_id, password, region)
+            await Node.connect()
 
-        kwargs["user_id"] = self.client.user.id
-        kwargs["host"] = LocalNodeProc.HOST
-        kwargs["port"] = LocalNodeProc.PORT
-        kwargs["password"] = LocalNodeProc.PASSWORD
+            self.Nodes.append(Node)
+            Node.dispatcher.on("VC_DESTROYED", self._onVCDestroyed)
+            Node.dispatcher.onAny(self._onAnyNodeEvent)
 
-        Node = NodeClient(self, *args, **kwargs)
-        await Node.connect()
+        return self.loop.create_task(
+            connectNode(self, host, port, password, region, launchOptions)
+        )
 
-        log.info(f"registering Node {Node.host}:{Node.port}")
+    async def _onVCDestroyed(self, data):
+        guild = self.client.get_guild(int(data["guild_id"]))
+        ws = self.getWebsocket(guild.shard_id)
 
-        self.Nodes.append(Node)
+        await ws.voice_state(guild.id, None)
 
-        Node.dispatcher.on("VC_DESTROYED", self._vc_destroyed)
-        Node.dispatcher.onAny(self._node_event)
+    async def _onAnyNodeEvent(self, event, data):
+        if not isinstance(data, dict) or not "guild_id" in data:
+            return
 
-        return self
+        guild = self.client.get_guild(int(data["guild_id"]))
+        vc = self.getVC(guild, safe=True)
 
-    async def _vc_destroyed(self, Data: dict) -> None:
-        guild = self.client.get_guild(int(Data["guild_id"]))
-        ws = self.__get_websocket(guild.shard_id)
+        if not vc:
+            return
 
-        await ws.voice_state(guild.id, None)
+        self.dispatcher.dispatch(event, vc, data)
 
-    async def _node_event(self, Event: str, Data: dict) -> None:
-        if not isinstance(Data, dict) or not "guild_id" in Data:
-            return
+    def getBestNode(self, exceptNode=None):
+        r"""Get the node with the fewest connected players.
 
-        guild = self.client.get_guild(int(Data["guild_id"]))
-        vc = self.getVC(guild)
+        :param Optional[discodo.Node] exceptNode: The host to except from the list.
 
-        self.dispatcher.dispatch(Event, vc, Data)
+        :rtype: discodo.Node"""
 
-    def getBestNode(self):
         SortedWithPerformance = sorted(
             [Node for Node in self.Nodes if Node.is_connected],
             key=lambda Node: len(Node.voiceClients),
         )
 
+        if exceptNode and exceptNode in SortedWithPerformance:
+            SortedWithPerformance.remove(exceptNode)
+
         return SortedWithPerformance[0] if SortedWithPerformance else None
 
     @property
     def voiceClients(self):
+        r"""A property that returns all voice clients from all of connected nodes.
+
+        :rtype: dict"""
+
         return dict(
             list(
-                chain.from_iterable(
+                itertools.chain.from_iterable(
                     [
                         Node.voiceClients.items()
                         for Node in self.Nodes
@@ -174,9 +215,17 @@ def voiceClients(self):
             )
         )
 
-    def getVC(
-        self, guild: Union[discord.Guild, int], safe: bool = False
-    ) -> VoiceClient:
+    def getVC(self, guild, safe=False):
+        r"""Get a voice client from the guild.
+
+        :param guild: Guild or guild ID from which to get the voice client.
+        :type guild: int or discord.Guild
+        :param bool safe: Whether to raise an exception when the voice client cannot be found, defaults to ``False``.
+
+        :raises discodo.VoiceClientNotFound: The voice client was not found and the ``safe`` value is ``False``.
+
+        :rtype: discodo.VoiceClient"""
+
         if isinstance(guild, discord.Guild):
             guild = guild.id
 
@@ -185,45 +234,96 @@ def getVC(
 
         return self.voiceClients.get(int(guild))
 
-    def __get_websocket(self, id: int):
+    def getWebsocket(self, id):
+        r"""Get a websocket object of the shard from discord.py
+
+        :param int id: The shard id to get a object.
+
+        :rtype: discord.gateway.DiscordWebSocket"""
+
         if isinstance(self.client, discord.AutoShardedClient):
             return self.client.shards[id].ws
         elif not self.client.shard_id or self.client.shard_id == id:
             return self.client.ws
 
-    async def connect(self, channel: discord.VoiceChannel) -> None:
-        log.info(f"connecting to {channel.id} of {channel.guild.id}")
+    async def connect(
+        self, channel: discord.VoiceChannel, node: NodeClient = None
+    ) -> None:
+        r"""Connect to the voice channel.
+
+        :param discord.VoiceChannel channel: The channel to connect to.
+        :param Optional[discodo.Node] node: The node to connect with, defaults to ``getBestNode()``
+
+        :raise ValueError: The ``channel`` value has no ``guild`` property.
+        :raise discodo.NodeNotConnected: There is no discodo node that is connected.
+        :raise asyncio.TimeoutError: The connection is not established in 10 seconds.
+
+        :rtype: discodo.VoiceClient"""
+
         if not hasattr(channel, "guild"):
             raise ValueError
 
-        if not self.getBestNode():
-            raise NodeNotConnected
+        if not node:
+            if not self.getBestNode():
+                raise NodeNotConnected
 
-        ws = self.__get_websocket(channel.guild.shard_id)
+            node = self.getBestNode()
 
-        await ws.voice_state(channel.guild.id, channel.id)
+        self.GuildReservationMap[channel.guild.id] = node
 
-        VC, _ = await self.dispatcher.wait_for(
-            "VC_CREATED",
-            lambda _, Data: int(Data["guild_id"]) == channel.guild.id,
-            timeout=10.0,
+        VC = self.getVC(channel.guild, safe=True)
+
+        if VC and VC.Node != node:
+            await VC.destroy()
+
+        Task = (
+            self.loop.create_task(
+                self.dispatcher.wait_for(
+                    "VC_CREATED",
+                    lambda _, Data: int(Data["guild_id"]) == channel.guild.id,
+                    timeout=10.0,
+                )
+            )
+            if not VC or VC.Node != node
+            else None
+        )
+
+        await self.getWebsocket(channel.guild.shard_id).voice_state(
+            channel.guild.id, channel.id
         )
 
+        if Task:
+            VC, _ = await Task
+
+        if self.GuildReservationMap.get(channel.guild.id) == node:
+            del self.GuildReservationMap[channel.guild.id]
+
         return VC
 
     async def disconnect(self, guild: discord.Guild) -> None:
-        log.info(f"disconnecting voice of {guild.id} without destroying")
-        ws = self.__get_websocket(guild.shard_id)
+        r"""Disconnect from the voice channel.
+
+        .. note::
+            This coroutine doesn't destroy the voice client. Recommand to use :meth:`destroy`
+
+        :param discord.Guild guild: The guild to disconnect from."""
+
+        ws = self.getWebsocket(guild.shard_id)
 
         await ws.voice_state(guild.id, None)
 
     async def destroy(self, guild: discord.Guild) -> None:
-        log.info(f"destroying voice client of {guild.id}")
+        r"""Destroy the voice client and disconnect from the voice channel
+
+        :param discord.Guild guild: The guild to destroy the voice client.
+
+        :raises discodo.VoiceClientNotFound: The voice client was not found."""
+
         if not guild.id in self.voiceClients:
             raise VoiceClientNotFound
 
         vc = self.getVC(guild.id)
-        ws = self.__get_websocket(guild.shard_id)
+        ws = self.getWebsocket(guild.shard_id)
 
         await ws.voice_state(guild.id, None)
         await vc.destroy()
diff --git a/discodo/client/__init__.py b/discodo/client/__init__.py
index f5a19eae..fd76035f 100644
--- a/discodo/client/__init__.py
+++ b/discodo/client/__init__.py
@@ -1,4 +1,6 @@
-from .node import Node
+from . import models
+from .node import Node, Nodes
+from .voice_client import VoiceClient
 
 try:
     import discord
diff --git a/discodo/client/gateway.py b/discodo/client/gateway.py
index 6239665e..20b8d775 100644
--- a/discodo/client/gateway.py
+++ b/discodo/client/gateway.py
@@ -1,82 +1,29 @@
 import asyncio
-import concurrent.futures
+import collections
 import json
 import logging
-import threading
 import time
 import warnings
-from collections import deque
-from typing import Any
+from typing import Optional
 
 import websockets
 
 from .. import __version__
 
-log = logging.getLogger("discodo.client")
+log = logging.getLogger("discodo.client.gateway")
 
 
-class keepAlive(threading.Thread):
-    def __init__(self, ws, interval: int, *args, **kwargs) -> None:
-        threading.Thread.__init__(self, *args, **kwargs)
-        self.daemon = True
-
-        self.ws = ws
-        self.interval = interval
-        self.Stopped = threading.Event()
-        self.latency = None
-        self.recent_latencies = deque(maxlen=20)
-
-        self._lastAck = self._lastSend = time.perf_counter()
-        self.timeout = ws.heartbeatTimeout
-        self.threadId = ws.threadId
-
-    def run(self) -> None:
-        while not self.Stopped.wait(self.interval):
-            if (self._lastAck + self.timeout) < time.perf_counter():
-                Runner = asyncio.run_coroutine_threadsafe(
-                    self.ws.close(4000), self.ws.loop
-                )
-
-                try:
-                    Runner.result()
-                except:
-                    pass
-
-                self.stop()
-                return
+class NodeConnection(websockets.client.WebSocketClientProtocol):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
 
-            payload = {"op": "HEARTBEAT", "d": int(time.time() * 1000)}
-            Runner = asyncio.run_coroutine_threadsafe(
-                self.ws.sendJson(payload), self.ws.loop
-            )
-            try:
-                totalBlocked = 0
-                while True:
-                    try:
-                        Runner.result(10)
-                        break
-                    except concurrent.futures.TimeoutError:
-                        totalBlocked += 10
-                        log.warning(
-                            f"Heartbeat blocked for more than {totalBlocked} seconds."
-                        )
-            except:
-                return self.stop()
-            else:
-                self._lastSend = time.perf_counter()
-
-    def ack(self) -> None:
-        self._lastAck = time.perf_counter()
-        self.latency = self._lastAck - self._lastSend
-        self.recent_latencies.append(self.latency)
-
-    def stop(self) -> None:
-        self.Stopped.set()
+        self.keepAliver = None
+        self.heartbeatTimeout = 60.0
 
+        self.latency: Optional[float] = None
+        self.recent_latencies = collections.deque(maxlen=20)
 
-class NodeConnection(websockets.client.WebSocketClientProtocol):
-    def __del__(self) -> None:
-        self.loop.call_soon_threadsafe(lambda: self.loop.create_task(self.close()))
+        self._lastAck = self._lastSend = time.perf_counter()
 
     @classmethod
     async def connect(cls, node, timeout=10.0):
@@ -91,58 +38,55 @@ async def connect(cls, node, timeout=10.0):
         )
 
         ws.Node = node
-        ws.keepAliver = None
-        ws.heartbeatTimeout = 60.0
-        ws.threadId = threading.get_ident()
 
         return ws
 
+    def __del__(self):
+        self.loop.call_soon_threadsafe(lambda: self.loop.create_task(self.close()))
+
     @property
     def is_connected(self) -> bool:
         return self.open and not self.closed
 
     @property
-    def latency(self) -> float:
-        return self._keepAliver.latency if self._keepAliver else None
+    def averagedLatency(self) -> Optional[float]:
+        return sum(self.recent_latencies) / len(self.recent_latencies)
 
-    @property
-    def averageLatency(self) -> float:
-        if not self._keepAliver:
-            return None
+    async def sendJson(self, data: dict) -> None:
+        await self.send(json.dumps(data))
 
-        return sum(self._keepAliver.recent_latencies) / len(
-            self._keepAliver.recent_latencies
-        )
+    async def send(self, data):
+        log.debug(f"{self.Node.host}:{self.Node.port} < {data}")
+        return await super().send(data)
+
+    async def handleHeartbeat(self):
+        while True:
+            if self._lastAck + self.heartbeatTimeout < time.perf_counter():
+                await self.close(4000)
+                return
 
-    async def sendJson(self, data) -> None:
-        log.debug(f"send to websocket {data}")
-        await super().send(json.dumps(data))
+            await self.sendJson({"op": "HEARTBEAT", "d": int(time.time() * 1000)})
 
-    async def send(self, Operation: dict, Data: Any = None) -> None:
-        payload = {"op": Operation, "d": Data}
+            self._lastSend = time.perf_counter()
 
-        await self.sendJson(payload)
+            await asyncio.sleep(self.heartbeatInterval)
 
-    async def poll(self) -> None:
-        message = await asyncio.wait_for(self.recv(), timeout=30.0)
+    async def poll(self):
+        message = json.loads(await asyncio.wait_for(self.recv(), timeout=30.0))
 
-        JsonData = json.loads(message)
+        log.debug(f"{self.Node.host}:{self.Node.port} > {message}")
 
-        Operation, Data = JsonData["op"], JsonData["d"]
+        Operation, Data = message["op"], message.get("d")
 
         if Operation == "HELLO":
             await self.HELLO(Data)
         elif Operation == "HEARTBEAT_ACK":
-            await self.HEARTBEAT_ACK(Data)
+            self._lastAck = time.perf_counter()
+            self.latency = self._lastAck - self._lastSend
+            self.recent_latencies.append(self.latency)
 
         return Operation, Data
 
-    async def close(self, *args, **kwargs) -> None:
-        if self.keepAliver:
-            self.keepAliver.stop()
-
-        await super().close(*args, **kwargs)
-
     async def HELLO(self, Data: dict) -> None:
         if Data.get("version") != __version__:
             warnings.warn(
@@ -150,8 +94,15 @@ async def HELLO(self, Data: dict) -> None:
                 UserWarning,
             )
 
-        self._keepAliver = keepAlive(self, min(Data["heartbeat_interval"], 5.0))
-        self._keepAliver.start()
+        self.heartbeatInterval = min(Data["heartbeat_interval"], 5.0)
+
+        if self.keepAliver and not self.keepAliver.done():
+            self.keepAliver.cancel()
+
+        self.keepAliver = self.loop.create_task(self.handleHeartbeat())
 
-    async def HEARTBEAT_ACK(self, Data: dict) -> None:
-        self._keepAliver.ack()
+    async def close(self, *args, **kwargs):
+        if self.keepAliver and not self.keepAliver.done():
+            self.keepAliver.cancel()
+
+        await super().close(*args, **kwargs)
diff --git a/discodo/client/http.py b/discodo/client/http.py
index 64f5719f..c07c2ef0 100644
--- a/discodo/client/http.py
+++ b/discodo/client/http.py
@@ -1,18 +1,13 @@
-from http.client import responses
-from typing import Union
+import logging
 
-import aiohttp
+from ..errors import HTTPException
+from .models import ensureQueueObjectType
 
-from ..errors import DiscodoException
-
-
-class HTTPException(DiscodoException):
-    def __init__(self, status: int) -> None:
-        super().__init__(f"{status} {responses.get(status, 'Unknown Status Code')}")
+log = logging.getLogger("discodo.client.http")
 
 
 class HTTPClient:
-    def __init__(self, client) -> None:
+    def __init__(self, client):
         self.VoiceClient = client
         self.Node = client.Node
         self.loop = client.Node.loop
@@ -26,82 +21,82 @@ def headers(self) -> dict:
             "VoiceClient-ID": str(self.VoiceClient.id),
         }
 
-    async def fetch(self, method: str, endpoint: str, **kwargs) -> dict:
+    async def fetch(self, method, endpoint, **kwargs):
         URL = self.Node.URL + endpoint
 
-        async with aiohttp.ClientSession(headers=self.headers) as session:
-            async with session.request(method, URL, **kwargs) as response:
-                if 200 <= response.status < 300:
-                    return await response.json(content_type=None)
+        if "headers" not in kwargs:
+            kwargs["headers"] = {}
 
-                raise HTTPException(response.status)
+        kwargs["headers"].update(self.headers)
 
-    async def getVCContext(self) -> dict:
-        return (await self.fetch("GET", "/getVCContext"))["source"]
+        async with self.Node.session.request(method, URL, **kwargs) as response:
+            log.debug(f"{method} {URL} with {kwargs} has returned {response.status}")
 
-    async def setVCContext(self, context: dict) -> dict:
-        return (await self.fetch("POST", "/setVCContext", json={"context": context}))[
-            "context"
-        ]
+            data = ensureQueueObjectType(
+                self.VoiceClient, await response.json(content_type=None)
+            )
 
-    async def getSource(self, query: str) -> dict:
-        return (await self.fetch("GET", "/getSource", params={"query": query}))[
-            "source"
-        ]
+            if 200 <= response.status < 300:
+                return data
 
-    async def searchSource(self, query: str) -> list:
-        return (await self.fetch("GET", "/searchSource", params={"query": query}))[
-            "sources"
-        ]
+            raise HTTPException(response.status, data)
 
-    async def putSource(self, source: dict) -> Union[int, list]:
-        return (await self.fetch("POST", "/putSource", json={"source": dict(source)}))[
-            "index"
-        ]
+    async def getSource(self, query):
+        return await self.fetch("GET", "/getSource", params={"query": query})
 
-    async def loadSource(self, query: str) -> dict:
-        return (await self.fetch("POST", "/loadSource", json={"query": str(query)}))[
-            "source"
-        ]
+    async def searchSources(self, query):
+        return await self.fetch("GET", "/searchSources", params={"query": query})
 
-    async def setVolume(self, volume: float) -> None:
-        return await self.fetch("POST", "/setVolume", json={"volume": float(volume)})
+    async def getVCContext(self):
+        return await self.fetch("GET", "/context")
 
-    async def setCrossfade(self, crossfade: float) -> None:
-        return await self.fetch(
-            "POST", "/setCrossfade", json={"crossfade": float(crossfade)}
-        )
+    async def setVCContext(self, data):
+        return await self.fetch("POST", "/context", json={"context": data})
 
-    async def setAutoplay(self, autoplay: bool) -> None:
-        return await self.fetch(
-            "POST", "/setAutoplay", json={"autoplay": bool(autoplay)}
-        )
+    async def putSource(self, source):
+        return await self.fetch("POST", "/putSource", json={"source": source})
 
-    async def setFilter(self, filter: dict) -> None:
-        return await self.fetch("POST", "/setFilter", json={"filter": dict(filter)})
+    async def loadSource(self, query):
+        return await self.fetch("POST", "/loadSource", json={"query": query})
 
-    async def seek(self, offset: float) -> None:
-        return await self.fetch("POST", "/seek", json={"offset": float(offset)})
+    async def getOptions(self):
+        return await self.fetch("GET", "/options")
 
-    async def skip(self, offset: int) -> None:
-        return (await self.fetch("POST", "/skip", json={"offset": int(offset)}))[
-            "remain"
-        ]
+    async def setOptions(self, options):
+        return await self.fetch("POST", "/options", json=options)
 
-    async def pause(self) -> None:
-        return await self.fetch("POST", "/pause")
+    async def getSeek(self):
+        return await self.fetch("GET", "/seek")
+
+    async def seek(self, offset):
+        return await self.fetch("POST", "/seek", json={"offset": offset})
+
+    async def skip(self, offset):
+        return await self.fetch("POST", "/skip", json={"offset": offset})
 
-    async def resume(self) -> None:
+    async def pause(self):
         return await self.fetch("POST", "/pause")
 
-    async def shuffle(self) -> None:
+    async def resume(self):
+        return await self.fetch("POST", "/resume")
+
+    async def shuffle(self):
         return await self.fetch("POST", "/shuffle")
 
-    async def remove(self, index: int) -> None:
-        return await self.fetch("POST", "/remove", json={"index": int(index)})
+    async def queue(self):
+        return await self.fetch("GET", "/queue")
+
+    async def getCurrent(self):
+        return await self.fetch("GET", "/current")
+
+    async def getQueueSource(self, tag):
+        return await self.fetch("GET", f"/queue/{tag}")
+
+    async def setCurrent(self, data):
+        return await self.fetch("POST", "/current", json=data)
 
-    async def getState(self) -> dict:
-        return await self.fetch("GET", "/state")
+    async def setQueueSource(self, tag, data):
+        return await self.fetch("POST", f"/queue/{tag}", json=data)
 
-    async def getQueue(self) -> dict:
-        return (await self.fetch("GET", "/queue"))["entries"]
+    async def removeQueueSource(self, tag):
+        return await self.fetch("DELETE", f"/queue/{tag}")
diff --git a/discodo/client/models.py b/discodo/client/models.py
new file mode 100644
index 00000000..cbdc903b
--- /dev/null
+++ b/discodo/client/models.py
@@ -0,0 +1,344 @@
+import functools
+import logging
+import time
+
+from ..enums import PlayerState
+
+log = logging.getLogger("discodo.client.models")
+
+
+def isInQueue(func):
+    @functools.wraps(func)
+    async def wrapper(self, *args, **kwargs):
+        if self not in self.VoiceClient.Queue:
+            raise ValueError("this source is not in queue.")
+
+        return await func(self, *args, **kwargs)
+
+    return wrapper
+
+
+def isNotInQueue(func):
+    @functools.wraps(func)
+    async def wrapper(self, *args, **kwargs):
+        if self in self.VoiceClient.Queue:
+            raise ValueError("this source is already in queue.")
+
+        return await func(self, *args, **kwargs)
+
+    return wrapper
+
+
+class AudioData:
+    r"""Object with playback information
+
+    .. container:: operations
+
+        .. describe:: x == y
+
+            Checks if two AudioSources are equal.
+
+        .. describe:: x != y
+
+            Checks if two AudioSources are not equal.
+
+    :var str tag: The tag of the object
+    :var str id: The id of the source
+    :var Optional[str] title: The title of the source
+    :var Optional[str] webpage_url: The webpage url of the source
+    :var Optional[str] thumbnail: The thumbnail url of the source
+    :var Optional[str] url: The stream url of the source
+    :var Optional[int] duration: The duration of the source
+    :var bool is_live: Whether the source is live stream or not
+    :var Optional[str] uploader: The uploader of the source
+    :var Optional[str] description: The description of the source
+    :var dict subtitles: The description of the source
+    :var dict chapters: The description of the source
+    :var bool related: Whether this source is added by autoplay
+    :var dict context: The context of the object
+    :var float start_position: The start position of the source"""
+
+    def __init__(self, VoiceClient, data):
+        self.VoiceClient = VoiceClient
+
+        self.data = data
+
+    def __getattr__(self, key):
+        return self.data.get(key)
+
+    def __getitem__(self, key):
+        return self.data.get(key)
+
+    def get(self, *args, **kwargs):
+        return self.data.get(*args, **kwargs)
+
+    def toDict(self):
+        return self.data
+
+    def __repr__(self) -> str:
+        return f"<AudioData id={self.id} title='{self.title}' duration={self.duration}>"
+
+    def __eq__(self, other):
+        if not isinstance(other, (type(self), AudioSource)):
+            return False
+
+        return self.tag == other.tag
+
+    def __ne__(self, other):
+        if not isinstance(other, (type(self), AudioSource)):
+            return False
+
+        return self.tag == other.tag
+
+    @isNotInQueue
+    async def put(self):
+        r"""Put the source into the queue.
+
+        :raise ValueError: The source is already in the queue.
+
+        :rtype: AudioData"""
+
+        return await self.VoiceClient.putSource(self)
+
+    @isInQueue
+    async def getContext(self):
+        r"""Get the context from the node.
+
+        :rtype: dict"""
+
+        data = await self.VoiceClient.http.getQueueSource(self.tag)
+        self.context = data.get("context", {})
+
+        return self.context
+
+    @isInQueue
+    async def setContext(self, data):
+        r"""Set the context to the node.
+
+        :param dict data: The context to set.
+
+        :rtype: dict"""
+
+        data = await self.VoiceClient.http.setQueueSource(self.tag, {"context": data})
+        self.context = data.get("context", {})
+
+        return self.context
+
+    @isInQueue
+    async def moveTo(self, index):
+        r"""Move this source to index in the queue.
+
+        :param int index: The index to move to
+
+        :rtype: AudioData"""
+
+        await self.VoiceClient.http.setQueueSource(self.tag, {"index": index})
+
+        return self
+
+    @isInQueue
+    async def seek(self, offset):
+        r"""Seek this source to offset.
+
+        :param float offset: The position to start playing
+
+        :rtype: AudioData"""
+
+        await self.VoiceClient.http.setQueueSource(self.tag, {"start_position": offset})
+
+        return self
+
+    @isInQueue
+    async def remove(self):
+        r"""Remove this source from the queue.
+
+        :rtype: AudioData"""
+
+        await self.VoiceClient.http.removeQueueSource(self.tag)
+
+        return self
+
+
+class AudioSource:
+    r"""Object with playback information that is loaded
+
+    .. container:: operations
+
+        .. describe:: x == y
+
+            Checks if two AudioSources are equal.
+
+        .. describe:: x != y
+
+            Checks if two AudioSources are not equal.
+
+    :var str tag: The tag of the object
+    :var str id: The id of the source
+    :var Optional[str] title: The title of the source
+    :var Optional[str] webpage_url: The webpage url of the source
+    :var Optional[str] thumbnail: The thumbnail url of the source
+    :var Optional[str] url: The stream url of the source
+    :var float duration: The duration of the source
+    :var bool is_live: Whether the source is live stream or not
+    :var Optional[str] uploader: The uploader of the source
+    :var Optional[str] description: The description of the source
+    :var dict subtitles: The description of the source
+    :var dict chapters: The description of the source
+    :var bool related: Whether this source is added by autoplay
+    :var dict context: The context of the object
+    :var float start_position: The start position of the source
+    :var bool seekable: Whether the source is seekable or not
+    :var Optional[float] position: The current position of the source"""
+
+    def __init__(self, VoiceClient, data):
+        self.VoiceClient = VoiceClient
+
+        self.data = data
+
+    def __getattr__(self, key):
+        return self.data.get(key)
+
+    def __getitem__(self, key):
+        if key == "position":
+            return self.position
+
+        return self.data.get(key)
+
+    def get(self, key, *args, **kwargs):
+        if key == "position":
+            return self.position
+
+        return self.data.get(key, *args, **kwargs)
+
+    def toDict(self):
+        return self.data
+
+    @property
+    def position(self):
+        if self.VoiceClient.state != PlayerState.PAUSED:
+            self.data["position"] = round(
+                self.data["position"]
+                + (time.time() - self.as_of if "as_of" in self.data else 0),
+                2,
+            )
+            self.as_of = time.time()
+
+        return self.data["position"]
+
+    def __repr__(self) -> str:
+        return (
+            f"<AudioSource id={self.id} title='{self.title}' duration={self.duration}"
+            + (f" position={self.position}" if hasattr(self, "position") else "")
+            + f" seekable={self.seekable}>"
+        )
+
+    def __eq__(self, other):
+        if not isinstance(other, (AudioData, type(self))):
+            return False
+
+        return self.tag == other.tag
+
+    def __ne__(self, other):
+        if not isinstance(other, (AudioData, type(self))):
+            return False
+
+        return self.tag != other.tag
+
+    async def getContext(self):
+        r"""Get the context from the node.
+
+        :rtype: dict"""
+
+        if self in self.VoiceClient.Queue:
+            data = await self.VoiceClient.http.getQueueSource(self.tag)
+        else:
+            data = await self.VoiceClient.http.getCurrent()
+
+        self.context = data.get("context", {})
+
+        return self.context
+
+    async def setContext(self, data):
+        r"""Set the context to the node.
+
+        :param dict data: The context to set.
+
+        :rtype: dict"""
+
+        if self in self.VoiceClient.Queue:
+            data = await self.VoiceClient.http.setQueueSource(
+                self.tag, {"context": data}
+            )
+        else:
+            data = await self.VoiceClient.http.setCurrent({"context": data})
+
+        self.context = data.get("context", {})
+
+        return self.context
+
+
+ARGUMENT_MAPPING = {"AudioData": AudioData, "AudioSource": AudioSource}
+
+
+def ensureQueueObjectType(VoiceClient, argument):
+    if isinstance(argument, list):
+        return list(map(lambda x: ensureQueueObjectType(VoiceClient, x), argument))
+
+    _type = argument.get("_type") if isinstance(argument, dict) else None
+
+    typeObject = ARGUMENT_MAPPING.get(_type)
+
+    if not _type or not typeObject:
+        if isinstance(argument, dict):
+            return dict(
+                list(
+                    map(
+                        lambda x: (x[0], ensureQueueObjectType(VoiceClient, x[1])),
+                        argument.items(),
+                    )
+                )
+            )
+
+        return argument
+
+    return typeObject(VoiceClient, argument)
+
+
+class Queue(list):
+    log = logging.getLogger("discodo.client.queue")
+    METHOD_REDIRECTION = {"setItem": "__setitem__", "delItem": "__delitem__"}
+
+    def __init__(self, VoiceClient):
+        super().__init__()
+
+        self.VoiceClient = VoiceClient
+        self.__checkArgumentType = functools.partial(ensureQueueObjectType, VoiceClient)
+
+    @staticmethod
+    def __readonly(*args, **kwargs):
+        raise RuntimeError("Queue is readonly object.")
+
+    __setitem__ = (
+        __delitem__
+    ) = pop = remove = append = clear = extend = insert = reverse = __readonly
+
+    def handleGetQueue(self, data):
+        entries = list(map(self.__checkArgumentType, data.get("entries", [])))
+
+        if not entries:
+            return
+
+        super().clear()
+        super().extend(entries)
+
+    def handleQueueEvent(self, data):
+        name, args = (
+            self.METHOD_REDIRECTION.get(data["name"]) or data["name"],
+            list(map(self.__checkArgumentType, data["args"])),
+        )
+
+        if not hasattr(super(), name):
+            self.log.warning(f"QUEUE_EVENT method {name} not found, ignored.")
+            return
+
+        return getattr(super(), name)(*args)
diff --git a/discodo/client/node.py b/discodo/client/node.py
index a92dd980..6e2a3654 100644
--- a/discodo/client/node.py
+++ b/discodo/client/node.py
@@ -4,19 +4,18 @@
 import os
 import secrets
 import sys
-from typing import Any, Coroutine
 
 import aiohttp
-from websockets.exceptions import ConnectionClosed
+from websockets import ConnectionClosed
 
 from .. import __dirname
 from ..errors import NodeNotConnected, VoiceClientNotFound
 from ..utils import EventDispatcher, tcp
 from .gateway import NodeConnection
-from .http import HTTPException
+from .models import ensureQueueObjectType
 from .voice_client import VoiceClient
 
-log = logging.getLogger("discodo.client")
+log = logging.getLogger("discodo.client.node")
 
 LocalNodeProc = None
 
@@ -35,16 +34,14 @@ async def launchLocalNode(**options):
     options["PORT"] = tcp.getFreePort()
     options["PASSWORD"] = secrets.token_hex()
 
-    log.info(f"launching local node process on {options['HOST']}:{options['PORT']}")
+    log.info(
+        f"trying to spawn local node process on {options['HOST']}:{options['PORT']}"
+    )
 
     os.environ["PYTHONPATH"] = os.path.dirname(__dirname)
 
     LocalNodeProc = await asyncio.create_subprocess_exec(
-        sys.executable,
-        "-m",
-        "discodo",
-        "--config-json",
-        json.dumps(options),
+        sys.executable, "-m", "discodo", "--config-json", json.dumps(options)
     )
 
     LocalNodeProc.HOST = options["HOST"]
@@ -52,9 +49,11 @@ async def launchLocalNode(**options):
     LocalNodeProc.PASSWORD = options["PASSWORD"]
 
     if LocalNodeProc.returncode:
-        raise SystemError("Cannot launch discodo subprocess.")
+        log.debug(
+            f"while launching local node process, returned {LocalNodeProc.returncode}"
+        )
 
-    log.info(f"Local node process launched on {options['HOST']}:{options['PORT']}")
+        raise SystemError("Cannot launch discodo subprocess.")
 
     loop = asyncio.get_event_loop()
 
@@ -76,15 +75,35 @@ async def launchLocalNode(**options):
 
 
 class Node:
+    r"""Represents a discodo node connection.
+
+    :var discodo.DPYClient client: The client which the node is binded.
+    :var Optional ws: The websocket gateway the client is currently connected to.
+    :var EventDispatcher dispatcher: The event dispatcher that the client dispatches events.
+    :var asyncio.AbstractEventLoop loop: The event loop that the client uses for operation.
+    :var str host: The host of the node to connect to.
+    :var int port: The port of the node to connect to.
+    :var str password:  The password of the node to connect to.
+    :var int user_id: This bot's ID
+    :var Optional[int] shard_id: This bot's shard ID, could be ``None``
+    :var Optional[str] region: Region set when registering a node
+    :var dict voiceClients: A dictionary consisting of pairs of guild IDs and voice clients."""
+
     def __init__(
         self,
-        host: str,
-        port: int,
-        user_id: int = None,
-        password: str = "hellodiscodo",
-        region: str = None,
-    ) -> None:
+        client,
+        host,
+        port,
+        user_id,
+        shard_id=None,
+        password="hellodiscodo",
+        region=None,
+    ):
+        self.client = client
+
         self.ws = None
+        self.session = aiohttp.ClientSession()
+
         self.dispatcher = EventDispatcher()
         self.dispatcher.onAny(self.onAnyEvent)
 
@@ -95,12 +114,18 @@ def __init__(
         self.password = password
 
         self.user_id = user_id
+        self.shard_id = shard_id
         self.region = region
 
         self._polling = None
         self.voiceClients = {}
         self.connected = asyncio.Event()
 
+    def __del__(self):
+        self.loop.call_soon_threadsafe(
+            lambda: self.loop.create_task(self.session.close())
+        )
+
     def __repr__(self) -> str:
         return (
             f"<Node connected={self.is_connected} host={self.host} port={self.port}"
@@ -109,17 +134,37 @@ def __repr__(self) -> str:
 
     @property
     def URL(self) -> str:
+        r"""Represents the restful api url of the node.
+
+        :rtype: str"""
+
         return f"http://{self.host}:{self.port}"
 
     @property
     def WS_URL(self) -> str:
+        r"""Represents the websocket url of the node.
+
+        :rtype: str"""
+
         return f"ws://{self.host}:{self.port}/ws"
 
-    async def connect(self) -> None:
+    @property
+    def is_connected(self) -> bool:
+        r"""Represents whether the node is connected.
+
+        :rtype: bool"""
+
+        return self.connected.is_set() and self.ws and self.ws.is_connected
+
+    async def connect(self):
+        r"""Connect to the node.
+
+        :raises ValueError: The node is already connected."""
+
         if self.connected.is_set():
             raise ValueError("Node already connected")
 
-        if self.ws and not self.ws.closed:
+        if self.ws and self.ws.is_connected:
             await self.ws.close()
 
         self.ws = await NodeConnection.connect(self)
@@ -129,45 +174,30 @@ async def connect(self) -> None:
         if not self._polling or self._polling.done():
             self._polling = self.loop.create_task(self.pollingWS())
 
-        if self.user_id:
-            await self.ws.send("IDENTIFY", {"user_id": self.user_id})
-
-    @property
-    def is_connected(self) -> bool:
-        return self.connected.is_set() and self.ws and self.ws.is_connected
-
-    @property
-    def headers(self) -> dict:
-        return {
-            "Authorization": self.password,
-            "User-ID": str(self.user_id),
-        }
-
-    async def fetch(self, method: str, endpoint: str, **kwargs) -> dict:
-        URL = self.Node.URL + endpoint
-
-        async with aiohttp.ClientSession(headers=self.headers) as session:
-            async with session.request(method, URL, **kwargs) as response:
-                if 200 <= response.status < 300:
-                    return await response.json(content_type=None)
-
-                raise HTTPException(response.status)
+        await self.send(
+            "IDENTIFY", {"user_id": self.user_id, "shard_id": self.shard_id}
+        )
 
-    async def close(self) -> None:
+    async def close(self):
         """ some action to do after disconnected from node """
 
-        return
+        ...
+
+    async def destroy(self):
+        r"""Destroy the node and remove from the client."""
 
-    async def destroy(self) -> None:
         if self._polling and not self._polling.done():
             self._polling.cancel()
 
         if self.ws and not self.ws.closed:
             await self.ws.close()
+        self.ws = None
 
         self.connected.clear()
         self.voiceClients = {}
-        self.ws = None
+
+        if self in self.client.Nodes:
+            self.client.Nodes.remove(self)
 
     async def pollingWS(self) -> None:
         while True:
@@ -178,24 +208,36 @@ async def pollingWS(self) -> None:
 
                 if self.ws:
                     await self.ws.close()
+                self.ws = None
 
                 await self.close()
-
-                self.ws = None
                 self.voiceClients = {}
+
                 return
 
-            log.debug(f"event {Operation} dispatched from websocket with {Data}")
+            if Data and isinstance(Data, dict) and "guild_id" in Data:
+                VC = self.getVC(Data["guild_id"], safe=True)
+                if VC:
+                    Data = ensureQueueObjectType(VC, Data)
+
+                    VC.dispatcher.dispatch(Operation, Data)
 
             self.dispatcher.dispatch(Operation, Data)
 
-    def send(self, *args, **kwargs) -> Coroutine:
+    async def send(self, op, data=None):
+        """Send websocket payload to the node
+
+        :param str op: Operation name of the payload
+        :param Optional[dict] data: Operation data to send with
+
+        :raises discodo.NodeNotConnected: The node is not connnected."""
+
         if not self.ws:
             raise NodeNotConnected
 
-        return self.ws.send(*args, **kwargs)
+        return await self.ws.sendJson({"op": op, "d": data})
 
-    async def _resumed(self, Data: dict) -> None:
+    async def onResumed(self, Data):
         for voice_client in self.voiceClients:
             voice_client.__del__()
 
@@ -206,28 +248,38 @@ async def _resumed(self, Data: dict) -> None:
 
     async def onAnyEvent(self, Operation, Data):
         if Operation == "RESUMED":
-            await self._resumed(Data)
+            await self.onResumed(Data)
         if Operation == "VC_CREATED":
             guild_id = int(Data["guild_id"])
             self.voiceClients[guild_id] = VoiceClient(self, Data["id"], guild_id)
-
-        if Data and isinstance(Data, dict) and "guild_id" in Data:
-            vc = self.getVC(Data["guild_id"])
-            if vc:
-                vc.dispatcher.dispatch(Operation, Data)
-
         if Operation == "VC_DESTROYED":
             guild_id = int(Data["guild_id"])
             if guild_id in self.voiceClients:
                 self.voiceClients[guild_id].__del__()
 
-    def getVC(self, guildID: int, safe: bool = False) -> VoiceClient:
+    def getVC(self, guildID, safe=False):
+        r"""Get a voice client from the guild.
+
+        :param int guildID: guild ID from which to get the voice client.
+        :param bool safe: Whether to raise an exception when the voice client cannot be found, defaults to ``False``.
+
+        :raises discodo.VoiceClientNotFound: The voice client was not found and the ``safe`` value is ``False``.
+
+        :rtype: discodo.VoiceClient"""
+
         if not int(guildID) in self.voiceClients and not safe:
             raise VoiceClientNotFound
 
         return self.voiceClients.get(int(guildID))
 
-    async def discordDispatch(self, payload: dict) -> None:
+    async def discordDispatch(self, payload):
+        r"""Dispatch the discord payload to the node.
+
+        .. note::
+            If you are using :py:class:`discodo.DPYClient`, you don't have to use this.
+
+        :param dict payload: The event data from the discord."""
+
         if not payload["t"] in [
             "READY",
             "RESUME",
@@ -238,36 +290,26 @@ async def discordDispatch(self, payload: dict) -> None:
 
         return await self.send("DISCORD_EVENT", payload)
 
-    async def getStatus(self) -> dict:
-        await self.send("GET_STATUS")
+    async def getStatus(self):
+        r"""Get status like cpu usage from the node with websocket.
 
-        return await self.dispatcher.wait_for("STATUS", timeout=10.0)
+        :rtype: dict"""
 
-    async def getContext(self, ws: bool = True) -> dict:
-        if ws:
-            await self.send("getSource")
+        await self.send("GET_STATUS")
 
-            return (await self.dispatcher.wait_for("getSource", timeout=10.0))[
-                "context"
-            ]
+        return await self.dispatcher.wait_for("STATUS", timeout=10.0)
 
-        return (await self.fetch("GET", "/getContext"))["context"]
 
-    async def setContext(self, context: dict, ws: bool = True) -> dict:
-        if ws:
-            await self.send("setContext", {"context": context})
+class Nodes(list):
+    r"""Represents a discodo node connection list.
 
-            return (await self.dispatcher.wait_for("setContext", timeout=10.0))[
-                "context"
-            ]
+    You can also use it like :py:class:`list`."""
 
-        return (await self.fetch("POST", "/setContext", json={"context": context}))[
-            "context"
-        ]
+    async def connect(self):
+        """Try to connect to all registered nodes.
 
+        :rtype: list"""
 
-class Nodes(list):
-    async def connect(self) -> Coroutine:
         task_list: list = list(map(lambda Node: Node.connect(), self))
 
         if not task_list:
@@ -277,7 +319,11 @@ async def connect(self) -> Coroutine:
 
         return list(map(lambda task: task.result(), Done))
 
-    async def getStatus(self) -> Coroutine:
+    async def getStatus(self):
+        """Try to get status from all registered nodes.
+
+        :rtype: list"""
+
         def get_task(Item):
             if Item.is_connected:
                 return Item.getStatus()
diff --git a/discodo/client/voice_client.py b/discodo/client/voice_client.py
index 215e9f16..60f2e038 100644
--- a/discodo/client/voice_client.py
+++ b/discodo/client/voice_client.py
@@ -1,174 +1,429 @@
 import asyncio
+import copy
 
+from ..enums import PlayerState
 from ..errors import NodeException
 from ..utils import EventDispatcher
 from .http import HTTPClient
+from .models import Queue
 
 
 class VoiceClient:
-    def __init__(self, Node, id: str, guild_id: int) -> None:
+    """Represents a voice connection of the guild.
+
+    :var discodo.Node Node: The node which the connection is connected with.
+    :var discodo.DPYClient client: The client which the connection is binded.
+    :var asyncio.AbstractEventLoop loop: The event loop that the client uses for operation.
+    :var str id: The id of the voice client, which is used on restful api.
+    :var int guild_id: The guild id which is connected to.
+    :var Optional[int] channel_id: The channel id which is connected to.
+    :var EventDispatcher dispatcher: The event dispatcher that the client dispatches events.
+    :var list Queue: The queue of the guild, it is synced to node and readonly."""
+
+    def __init__(self, Node, id, guild_id):
         self.Node = Node
+        self.client = Node.client
         self.loop = Node.loop
 
         self.id = id
         self.guild_id = guild_id
-        self.channel_id: int = None
+        self.channel_id = None
 
         self.http = HTTPClient(self)
         self.dispatcher = EventDispatcher()
 
-        self.dispatcher.on("VC_CHANNEL_EDITED", self.__channel_editied)
+        self.dispatcher.on(
+            "VC_CHANNEL_EDITED",
+            lambda data: setattr(self, "channel_id", data["channel_id"]),
+        )
+
+        self._state = None
+        self._volume = 1.0
+        self._crossfade = 10.0
+        self._autoplay = True
+        self._filter = {}
+        self._context = {}
+
+        self._current = None
+
+        self.dispatcher.on("getState", self.handleGetState)
+
+        self.syncTask = self.loop.create_task(self.syncWithNode())
+
+        self.Queue = Queue(self)
+
+        self.dispatcher.on("getQueue", self.Queue.handleGetQueue)
+        self.dispatcher.on("QUEUE_EVENT", self.Queue.handleQueueEvent)
+
+        self.loop.create_task(self.send("getQueue", {}))
 
     def __repr__(self) -> str:
         return f"<VoiceClient id={self.id} guild_id={self.guild_id} channel_id={self.channel_id} Node={self.Node}>"
 
     def __del__(self):
+        if self.syncTask and not self.syncTask.done():
+            self.syncTask.cancel()
+
         vc = self.Node.voiceClients.get(self.guild_id)
         if vc and vc == self:
             self.Node.voiceClients.pop(self.guild_id)
 
-    def __channel_editied(self, channel_id: int) -> None:
-        self.channel_id: int = channel_id
+    async def syncWithNode(self):
+        while True:
+            await self.send("getState", {})
+
+            await asyncio.sleep(5.0)
+
+    @property
+    def volume(self):
+        """Represents the volume of this guild.
+
+        The maximum value is ``2.0``
+
+        .. note::
+            This value is a percentage with decimal points. For example, ``100%`` is ``1.0``, and ``5%`` is ``0.05``.
+
+        :rtype: float"""
+
+        return self._volume
+
+    @property
+    def crossfade(self):
+        """Represents the crossfade duration of this guild.
+
+        :rtype: float"""
+
+        return self._crossfade
+
+    @property
+    def autoplay(self):
+        """Represents the autoplay state of this guild.
+
+        :rtype: bool"""
+
+        return self._autoplay
+
+    @property
+    def filter(self):
+        """Represents the autoplay state of this guild.
+
+        .. note::
+            For more information, check the documentation of the `FFmpeg Filter`_
+
+        .. _`FFmpeg Filter`: https://ffmpeg.org/ffmpeg-filters.html
+
+        :rtype: bool"""
+
+        return copy.copy(self._filter)
+
+    @property
+    def current(self):
+        return self._current
+
+    @property
+    def duration(self):
+        return self._current.duration if self._current else None
+
+    @property
+    def position(self):
+        return self._current.position if self._current else None
+
+    @property
+    def remain(self):
+        return round(
+            self._current.duration - self._current.position if self._current else None,
+            2,
+        )
+
+    @property
+    def context(self):
+        return self._context
+
+    @property
+    def state(self):
+        return self._state
+
+    def handleGetState(self, data):
+        options = data["options"]
 
-    async def send(self, Operation: str, Data: dict = {}):
-        Data["guild_id"] = self.guild_id
+        self._volume = options["volume"]
+        self._crossfade = options["crossfade"]
+        self._autoplay = options["autoplay"]
+        self._filter = options["filter"]
 
-        return await self.Node.send(Operation, Data)
+        self._current = data["current"]
+        self._context = data["context"]
+        self._state = PlayerState(data["state"])
 
-    async def query(
-        self, Operation: str, Data: dict = {}, Event: str = None, timeout: float = 10.0
-    ) -> dict:
-        if not Event:
-            Event = Operation
+        self.channel_id = data["channel_id"]
 
-        Future = self.loop.create_task(
+    async def send(self, op, data):
+        """Send websocket payload to the node with guild id
+
+        :param str op: Operation name of the payload
+        :param Optional[dict] data: Operation data to send with"""
+
+        data["guild_id"] = str(self.guild_id)
+
+        return await self.Node.send(op, data)
+
+    async def query(self, op, data=None, event=None, timeout=10.0):
+        """Send websocket payload to the node with guild id and await response.
+
+        :param str op: Operation name of the payload
+        :param Optional[dict] data: Operation data to send with
+        :param Optional[str] event: Event name to receive response, defaults to ``op``
+        :param Optional[float] timeout: Seconds to wait for response
+
+        :raise asyncio.TimeoutError: The query is timed out.
+        :raise discodo.NodeException: The node returned some exceptions.
+
+        :rtype: Any"""
+
+        if not event:
+            event = op
+        if not data:
+            data = {}
+
+        Task = self.loop.create_task(
             self.dispatcher.wait_for(
-                Event,
-                condition=lambda Data: int(Data["guild_id"]) == int(self.guild_id),
+                event,
+                condition=lambda d: d["guild_id"] == str(self.guild_id),
                 timeout=timeout,
             )
         )
 
-        await self.send(Operation, Data)
+        await self.send(op, data)
 
-        Data = await Future
+        Data = await Task
 
         if Data.get("traceback"):
             raise NodeException(*list(Data["traceback"].items())[0])
 
         return Data
 
-    async def getVCContext(self, ws: bool = True) -> dict:
-        if ws:
-            return (await self.query("getVCContext"))["context"]
+    async def fetchContext(self):
+        r"""Fetch the context from the node.
 
-        return await self.http.getVCContext()
+        :rtype: dict"""
 
-    async def setVCContext(self, context: dict, ws: bool = True) -> dict:
-        if ws:
-            return (await self.query("setVCContext", {"context": context}))["context"]
+        self._context = await self.http.getVCContext()
 
-        return await self.http.setVCContext(context)
+        return self.context
 
-    async def getSource(self, Query: str, ws: bool = True) -> dict:
-        if ws:
-            return (await self.query("getSource", {"query": Query}))["source"]
+    async def setContext(self, data):
+        r"""Set the context to the node.
 
-        return await self.http.getSource(Query)
+        :param dict data: The context to set.
 
-    async def searchSources(self, Query: str, ws: bool = True) -> list:
-        if ws:
-            return (await self.query("searchSources", {"query": Query}))["sources"]
+        :rtype: dict"""
 
-        return await self.http.searchSources(Query)
+        self._context = await self.http.setVCContext(data)
 
-    async def loadSource(self, Query: str, ws: bool = True) -> dict:
-        if ws:
-            return (await self.query("loadSource", {"query": Query}))["source"]
+        return self.context
 
-        return await self.http.loadSource(Query)
+    async def getSource(self, query):
+        r"""Search the query and get source from extractor
 
-    async def putSource(self, Source: dict, ws: bool = True) -> int:
-        if ws:
-            return (await self.query("putSource", {"song": Source}))["index"]
+        :param str query: The query to search.
 
-        return await self.http.putSource(Source)
+        :rtype: AudioData"""
 
-    async def skip(self, offset: int = 1, ws: bool = True) -> int:
-        if ws:
-            return (await self.query("skip", {"offset": offset}))["remain"]
+        data = await self.http.getSource(query)
+
+        return data["source"]
+
+    async def searchSources(self, query):
+        r"""Search the query and get sources from extractor
+
+        :param str query: The query to search.
+
+        :rtype: list[AudioData]"""
+
+        data = await self.http.searchSources(query)
+
+        return data["sources"]
+
+    async def putSource(self, source):
+        r"""Search the query and get sources from extractor
+
+        :param source: The source to put on the queue.
+        :type source: AudioData or AudioSource or list
+
+        :rtype: AudioData or AudioSource or list"""
+
+        data = await self.http.putSource(
+            list(map(lambda x: x.toDict(), source))
+            if isinstance(source, list)
+            else source.toDict()
+        )
+
+        return data["source"]
+
+    async def loadSource(self, query):
+        r"""Search the query and put source to the queue
+
+        :param str query: The query to search.
+
+        :rtype: AudioData or list"""
+
+        data = await self.http.loadSource(query)
+
+        return data["source"]
+
+    async def skip(self, offset=1):
+        r"""Skip the source
+
+        :param int offset: how many to skip the sources"""
 
         return await self.http.skip(offset)
 
-    async def seek(self, offset: float, ws: bool = True) -> dict:
-        if ws:
-            return await self.query("seek", {"offset": offset})
+    async def seek(self, offset):
+        r"""Seek the player
 
-        return await self.http.seek(offset)
+        :param float offset: The position to seek"""
 
-    async def setVolume(self, volume: int, ws: bool = True) -> float:
-        if ws:
-            return (await self.query("setVolume", {"volume": volume}))["volume"]
+        await self.http.seek(offset)
 
-        return await self.http.setVolume(volume)
+        await self.fetchState()
 
-    async def setCrossfade(self, crossfade: float, ws: bool = True) -> float:
-        if ws:
-            return (await self.query("setCrossfade", {"crossfade": crossfade}))[
-                "crossfade"
-            ]
+        return
 
-        return await self.http.setCrossfade(crossfade)
+    async def getOptions(self):
+        r"""Get options of the player
 
-    async def setAutoplay(self, autoplay: bool, ws: bool = True) -> bool:
-        if ws:
-            return (await self.query("setAutoplay", {"autoplay": autoplay}))["autoplay"]
+        :rtype: dict"""
 
-        return await self.http.setAutoplay(autoplay)
+        return await self.http.getOptions()
 
-    async def setFilter(self, filter: dict, ws: bool = True) -> dict:
-        if ws:
-            return await self.query("setFilter", {"filter": filter})
+    async def setOptions(self, **options):
+        r"""Set options of the player
 
-        return await self.http.setFilter(filter)
+        :param Optional[float] volume: The volume of the player to change.
+        :param Optional[float] crossfade: The crossfade of the player to change.
+        :param Optional[bool] autoplay: The autoplay state of the player to change.
+        :param Optional[dict] filter: The filter object of the player to change.
 
-    async def pause(self, ws: bool = True) -> dict:
-        if ws:
-            return await self.query("pause")
+        :rtype: dict"""
 
-        return await self.http.pause()
+        if "volume" in options:
+            self._volume = options["volume"]
+        if "crossfade" in options:
+            self._crossfade = options["crossfade"]
+        if "autoplay" in options:
+            self._autoplay = options["autoplay"]
+        if "filter" in options:
+            self._filter = options["filter"]
 
-    async def resume(self, ws: bool = True) -> dict:
-        if ws:
-            return await self.query("resume")
+        return await self.http.setOptions(options)
 
-        return await self.http.resume()
+    async def setVolume(self, volume):
+        r"""Set volume of the player
 
-    async def getQueue(self, ws: bool = True) -> list:
-        if ws:
-            return (await self.query("getQueue"))["entries"]
+        :param float volume: The volume of the player to change.
 
-        return await self.http.getQueue()
+        :rtype: dict"""
 
-    async def getState(self, ws: bool = True) -> dict:
-        if ws:
-            return await self.query("getState")
+        return await self.setOptions(volume=volume)
 
-        return await self.http.getState()
+    async def setCrossfade(self, crossfade):
+        r"""Set crossfade of the player
 
-    async def shuffle(self, ws: bool = True) -> dict:
-        if ws:
-            return await self.query("shuffle")
+        :param float crossfade: The crossfade of the player to change.
+
+        :rtype: dict"""
+
+        return await self.setOptions(crossfade=crossfade)
+
+    async def setAutoplay(self, autoplay):
+        r"""Set autoplay state of the player
+
+        :param bool autoplay: The autoplay state of the player to change.
+
+        :rtype: dict"""
+
+        return await self.setOptions(autoplay=autoplay)
+
+    async def setFilter(self, filter):
+        r"""Set filter of the player
+
+        :param float crossfade: The filter object of the player to change.
+
+        :rtype: dict"""
+
+        return await self.setOptions(filter=filter)
+
+    async def pause(self):
+        r"""Pause the player"""
+
+        await self.http.pause()
+
+        await self.fetchState()
+
+        return
+
+    async def resume(self):
+        r"""Resume the player"""
+
+        await self.http.resume()
+
+        await self.fetchState()
+
+        return
+
+    async def shuffle(self):
+        r"""Shuffle the queue
+
+        :rtype: list[AudioData or AudioSource]"""
+
+        data = await self.http.shuffle()
+
+        self.Queue.handleGetQueue(data)
 
-        return await self.http.shuffle()
+        return self.Queue
+
+    async def getCurrent(self):
+        r"""Fetch current playing source
+
+        :rtype: AudioSource"""
+
+        self._current = await self.http.getCurrent()
+
+        return self._current
+
+    async def fetchState(self):
+        r"""Fetch current player state.
+
+        :rtype: dict"""
+
+        data = await self.query("getState")
+
+        return data
+
+    async def fetchQueue(self, ws=True):
+        r"""Fetch queue to force refresh the internal queue.
+
+        :param Optional[bool] ws: Whether to request queue on websocket or not.
+
+        :rtype: list[AudioData or AudioSource]"""
 
-    async def remove(self, index: int, ws: bool = True) -> dict:
         if ws:
-            return await self.query("remove", {"index": index})
+            await self.query("getQueue")
+        else:
+            self.Queue.handleGetQueue(await self.http.queue())
+
+        return self.Queue
+
+    async def requestSubtitle(self, lang=None, url=None):
+        r"""Request to send synced subtitle to discodo node.
 
-        return await self.http.remove(index)
+        One of the parameters is required.
+
+        :param Optional[str] lang: The language to get subtitle.
+        :param Optional[str] url: The subtitle url to fetch.
+
+        :rtype: dict"""
 
-    async def requestSubtitle(self, lang: str = None, url: str = None) -> dict:
         if not any([lang, url]):
             raise ValueError("Either `lang` or `url` is needed.")
 
@@ -180,7 +435,17 @@ async def requestSubtitle(self, lang: str = None, url: str = None) -> dict:
 
         return await self.query("requestSubtitle", Data)
 
-    async def getSubtitle(self, *args, callback: callable, **kwargs):
+    async def getSubtitle(self, *args, callback, **kwargs):
+        r"""Request to send synced subtitle to discodo node and handle event to callback function.
+
+        ``lang`` or ``url`` is required.
+
+        :param callable callback: The callback function on subtitle event, must be coroutine function.
+        :param Optional[str] lang: The language to get subtitle.
+        :param Optional[str] url: The subtitle url to fetch.
+
+        :rtype: dict"""
+
         if not asyncio.iscoroutinefunction(callback):
             raise ValueError("Callback function must be coroutine function.")
 
@@ -215,5 +480,42 @@ async def lyricsDone(Data):
 
         return Data
 
-    async def destroy(self) -> dict:
-        return await self.query("VC_DESTROY", Event="VC_DESTROYED")
+    async def moveTo(self, node):
+        r"""Move the player's current Node.
+
+        :param discodo.Node node: The node to move to."""
+
+        if node == self.Node:
+            raise ValueError("Already connected to this node.")
+
+        channel = self.client.client.get_channel(self.channel_id)
+
+        if not channel:
+            raise ValueError("this voice client is not connected to the channel.")
+
+        await self.fetchState()
+
+        VC = await self.client.connect(channel, node)
+
+        await VC.setOptions(
+            volume=self.volume,
+            crossfade=self.crossfade,
+            autoplay=self.autoplay,
+            filter=self.filter,
+        )
+
+        if self.context:
+            await VC.setContext(self.context)
+
+        if self.current:
+            await VC.putSource(self.current)
+
+        if self.Queue:
+            await VC.putSource(self.Queue)
+
+        return VC
+
+    async def destroy(self):
+        r"""Destroy the client"""
+
+        return await self.query("VC_DESTROY", event="VC_DESTROYED")
diff --git a/discodo/config.py b/discodo/config.py
index 695791b5..77264003 100644
--- a/discodo/config.py
+++ b/discodo/config.py
@@ -1,5 +1,4 @@
-import uuid
-from typing import Union
+from typing import Optional, Union
 
 from .planner import RoutePlanner
 
@@ -38,8 +37,8 @@ def __init__(self) -> None:
         self.HOST: str = "0.0.0.0"
         self.PORT: int = 8000
         self.PASSWORD: str = "hellodiscodo"
-        self.HANDSHAKE_INTERVAL: str = 15
-        self.HANDSHAKE_TIMEOUT: str = 60.0
+        self.HANDSHAKE_INTERVAL: float = 15
+        self.HANDSHAKE_TIMEOUT: float = 60.0
 
         # NETWORK
         self.IPBLOCKS: list = []
@@ -66,15 +65,12 @@ def __init__(self) -> None:
         self.VCTIMEOUT: float = 300.0
 
         # EXTRA RESOLVER
-        self.ENABLED_EXT_RESOLVER: list = []  # melon, spotify, vibe
+        self.ENABLED_EXT_RESOLVER: list = []  # melon, spotify, vibe, local
 
         self.PLAYLIST_PAGE_LIMIT: int = 6
 
-        self.SPOTIFY_ID: str = None
-        self.SPOTIFY_SECRET: str = None
-
-        # ETC
-        self.RANDOM_STATE: str = str(uuid.uuid4())
+        self.SPOTIFY_ID: Optional[str] = None
+        self.SPOTIFY_SECRET: Optional[str] = None
 
     @property
     def SAMPLES_PER_FRAME(self) -> int:
diff --git a/discodo/connector.py b/discodo/connector.py
index 27fbfde6..7d4d3a3d 100644
--- a/discodo/connector.py
+++ b/discodo/connector.py
@@ -1,106 +1,101 @@
 import asyncio
+import contextlib
 import logging
 import re
 import socket
 import struct
 import threading
-import traceback
 import uuid
 
+import websockets
 from websockets.exceptions import ConnectionClosed
 
 from .config import Config
 from .enums import WebsocketCloseCode
+from .errors import EncryptModeNotReceived
 from .gateway import VoiceSocket
-from .natives import Cipher, opus
+from .natives import opus
+from .natives.encrypt import Cipher
 
-log = logging.getLogger("discodo.VoiceConnector")
+log = logging.getLogger("discodo.connector")
 
 
 class VoiceConnector:
-    def __init__(self, manager, data: dict = {}) -> None:
+    def __init__(self, manager, data=None):
+        self.id = str(uuid.uuid4())
         self.loop = manager.loop
+        self.manager = manager
 
         self.ws = self.socket = None
 
-        self.id = str(uuid.uuid4())
-        self.manager = manager
-
-        self.data = data
-        self.guild_id = data.get("guild_id")
+        self.data = data or {}
         self._channel_id = None
 
+        self.encoder = opus.Encoder()
         self.connectedEvent = asyncio.Event()
         self.connectedThreadEvent = threading.Event()
 
         self._polling = None
-        self._sequence = self._timestamp = 0
-        self.encoder = opus.Encoder()
+        self.endpointIp = self.endpointPort = None
+        self._sequence = self._timestamp = self.ssrc = 0
         self.speakState = False
+        self.encryptMode = None
 
         if data:
             self.loop.create_task(self.createSocket())
 
-    def __del__(self) -> None:
+    def __del__(self):
         if self.socket:
-            try:
+            with contextlib.suppress(Exception):
                 self.socket.close()
-            except:
-                pass
 
-        if self.ws:
+        if self.ws and not self.ws.closed:
             self.loop.create_task(self.ws.close(4000))
 
         if self._polling:
             self._polling.cancel()
 
     def __repr__(self) -> str:
-        return f"<VoiceConnector guild_id={self.guild_id} connected={self._connected.is_set()} sequence={self.sequence} timestamp={self.timestamp}>"
+        return f"<VoiceConnector guild_id={self.guild_id} connected={self.connectedEvent.is_set()} sequence={self.sequence} timestamp={self.timestamp}>"
 
     @property
-    def channel_id(self) -> int:
+    def channel_id(self):
         return self._channel_id
 
     @channel_id.setter
-    def channel_id(self, value: int) -> int:
-        self._channel_id: int = int(value)
+    def channel_id(self, value):
+        self._channel_id = str(value)
 
     @property
-    def user_id(self) -> int:
-        """
-        User id which is connected.
-
-        :getter: Returns User id
-        :rtype: int
-        """
-
+    def user_id(self) -> str:
         return self.manager.id
 
     @property
     def session_id(self) -> str:
-        """
-        Session id which is connected.
-
-        :getter: Returns Session id
-        :rtype: str
-        """
-
         return self.manager.session_id
 
     @property
-    def sequence(self) -> int:
-        """
-        Sequence value which is used to send voice packet.
+    def data(self):
+        return self._data
 
-        :getter: Returns sequence
-        :setter: Sets sequence value (max 65535)
-        :rtype: int
-        """
+    @data.setter
+    def data(self, value):
+        self._data = value
 
+        self.token = self._data.get("token")
+        self.endpoint = (
+            re.sub(r"(:[0-9]+)", "", self._data["endpoint"])
+            if self._data.get("endpoint")
+            else None
+        )
+        self.guild_id = self._data.get("guild_id")
+
+    @property
+    def sequence(self) -> int:
         return self._sequence
 
     @sequence.setter
-    def sequence(self, value: int) -> None:
+    def sequence(self, value: int):
         if self._sequence + value > 65535:
             self._sequence = 0
         else:
@@ -108,43 +103,22 @@ def sequence(self, value: int) -> None:
 
     @property
     def timestamp(self) -> int:
-        """
-        Timestamp value which is used to send voice packet.
-
-        :getter: Returns timestamp
-        :setter: Sets timestamp value (max 4294967295)
-        :rtype: int
-        """
-
         return self._timestamp
 
     @timestamp.setter
-    def timestamp(self, value: int) -> None:
+    def timestamp(self, value: int):
         if self._timestamp + value > 4294967295:
             self._timestamp = 0
         else:
             self._timestamp = value
 
-    async def createSocket(self, data: dict = None) -> None:
-        """
-        Create UDP socket with discord to send voice packet.
-
-        :param dict data: A VOICE_SERVER_UPDATE payload, defaults to None
-        :rtype: None
-        """
-
+    async def createSocket(self, data=None):
         self.connectedEvent.clear()
         self.connectedThreadEvent.clear()
 
         if data:
             self.data = data
 
-        self.guild_id = self.data.get("guild_id")
-
-        self.token = self.data.get("token")
-        self.endpoint = re.sub(r"(:[0-9]+)", "", self.data.get("endpoint"))
-        log.info(f"voice endpoint {self.endpoint} detected.")
-
         if self.socket:
             try:
                 self.socket.close()
@@ -154,15 +128,15 @@ async def createSocket(self, data: dict = None) -> None:
         self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         self.socket.setblocking(False)
 
-        if self.ws:
+        if self.ws and not self.ws.closed:
             await self.ws.close(4000)
 
         if hasattr(self, "secretKey"):
-            del self.secretKey
+            del self.secretKey  # type: ignore
 
         self.ws = await VoiceSocket.connect(self)
         while not hasattr(self, "secretKey"):
-            await self.ws.poll()
+            await self.ws.poll()  # type: ignore
 
         if not self._polling or self._polling.done():
             self._polling = self.loop.create_task(self.pollingWS())
@@ -170,18 +144,17 @@ async def createSocket(self, data: dict = None) -> None:
         self.connectedEvent.set()
         self.connectedThreadEvent.set()
 
-    async def pollingWS(self) -> None:
+    async def pollingWS(self):
         while True:
             try:
-                await self.ws.poll()
-            except (asyncio.TimeoutError, ConnectionClosed) as e:
+                await self.ws.poll()  # type: ignore
+            except (asyncio.TimeoutError, websockets.ConnectionClosed) as e:
                 self.connectedEvent.clear()
                 self.connectedThreadEvent.clear()
 
-                if not isinstance(e, ConnectionClosed):
-                    e.code = 1000
-
-                closeCode = WebsocketCloseCode(e.code)
+                closeCode = WebsocketCloseCode(
+                    e.code if isinstance(e, websockets.ConnectionClosed) else 1000
+                )
 
                 reason = (
                     f"with {closeCode.value}: {closeCode.name}"
@@ -190,7 +163,7 @@ async def pollingWS(self) -> None:
                 )
 
                 (log.warn if closeCode.warn else log.info)(
-                    f"voice connection of {self.guild_id} destroyed {reason}. {'resuming...' if closeCode.resume else 'wait for events.'}"
+                    f"voice connection of {self.guild_id} destroyed {reason}, {'resuming...' if closeCode.resume else 'wait for events.'}"
                 )
 
                 if closeCode.resume:
@@ -207,17 +180,14 @@ async def pollingWS(self) -> None:
                         )
 
                     return self.__del__()
+            except asyncio.CancelledError:
+                return self.__del__()
             except:
-                traceback.print_exc()
+                log.exception(
+                    "while polling websocket, an unhandled exception occured."
+                )
 
     def makePacket(self, data: bytes) -> bytes:
-        """
-        Converts and encrypts data to format.
-
-        :param bytes data: A packet to be converted
-        :rtype: bytes
-        """
-
         header = bytearray(12)
         header[0], header[1] = 0x80, 0x78
 
@@ -225,17 +195,12 @@ def makePacket(self, data: bytes) -> bytes:
         struct.pack_into(">I", header, 4, self.timestamp)
         struct.pack_into(">I", header, 8, self.ssrc)
 
-        return getattr(Cipher, self.encryptMode)(self.secretKey, header, data)
-
-    def send(self, data: bytes, encode: bool = True) -> None:
-        """
-        Send packet to discord.
+        if not self.encryptMode:
+            raise EncryptModeNotReceived
 
-        :param bytes data: A packet to be sent
-        :param bool encode: Whether data is encoded
-        :rtype: None
-        """
+        return getattr(Cipher, self.encryptMode)(self.secretKey, header, data)  # type: ignore
 
+    def send(self, data: bytes, encode: bool = True):
         self.sequence += 1
         if encode:
             data = self.encoder.encode(data)
diff --git a/discodo/enums.py b/discodo/enums.py
index f621efb7..cc656c53 100644
--- a/discodo/enums.py
+++ b/discodo/enums.py
@@ -1,6 +1,25 @@
 import enum
 
 
+class VoicePayload(enum.Enum):
+    UNKNOWN = -1
+    IDENTIFY = 0
+    SELECT_PROTOCOL = 1
+    READY = 2
+    HEARTBEAT = 3
+    SESSION_DESCRIPTION = 4
+    SPEAKING = 5
+    HEARTBEAT_ACK = 6
+    RESUME = 7
+    HELLO = 8
+    RESUMED = 9
+    CLIENT_DISCONNECT = 13
+
+    @classmethod
+    def _missing_(cls, _):
+        return cls.UNKNOWN
+
+
 class WebsocketCloseCode(enum.Enum):
     HEARTBEAT_TIMEOUT = 1000
     CLOUDFLARE = 1001
@@ -32,3 +51,18 @@ def warn(self) -> bool:
     @property
     def resume(self) -> bool:
         return self.value in self.RESUME_CODES.value
+
+
+class PlayerState(enum.Enum):
+    UNKNOWN = -1
+    DISCONNECTED = 0
+    STOPPED = 1
+    PAUSED = 2
+    PLAYING = 3
+
+    @classmethod
+    def __missing__(cls, _):
+        return cls.UNKNOWN
+
+    def toDict(self):
+        return self.value
diff --git a/discodo/errors.py b/discodo/errors.py
index 8d44a813..0df268ba 100644
--- a/discodo/errors.py
+++ b/discodo/errors.py
@@ -1,42 +1,93 @@
+from http.client import responses
+
+
 class DiscodoException(Exception):
-    pass
+    """The basic exception class of discodo"""
 
+    ...
 
-class Forbidden(DiscodoException):
-    pass
 
+class EncryptModeNotReceived(DiscodoException):
+    """Exception that is thrown when trying to send packet before receveing encrypt mode.
+
+    It's only raise in :py:class:`DiscordVoiceClient`"""
+
+    ...
+
+
+class NotPlaying(DiscodoException):
+    """Exception that is thrown when trying to operate something while not playing."""
+
+    ...
 
-class TooManyRequests(DiscodoException):
-    pass
+
+class VoiceClientNotFound(DiscodoException):
+    """Exception that is thrown when there is no voice client."""
+
+    ...
 
 
 class NoSearchResults(DiscodoException):
-    pass
+    """Exception that is thrown when there is no search results."""
 
+    ...
 
-class NodeException(DiscodoException):
-    def __init__(self, name, reason) -> None:
-        self.name = name
-        self.reason = reason
 
-        super().__init__(f"{name}{': ' if reason else ''}{reason}")
+class OpusLoadError(DiscodoException):
+    """Exception that is thrown when loading libopus failed."""
 
+    ...
 
-class VoiceClientNotFound(DiscodoException):
-    pass
 
+class HTTPException(DiscodoException):
+    """Exception that is thrown when HTTP operation failed.
+
+    :var int status: HTTP status code
+    :var str description: Description of the HTTP status code
+    :var str message: Server message with this request"""
 
-class NotConnected(DiscodoException):
-    pass
+    def __init__(self, status: int, data=None) -> None:
+        if not data:
+            data = {}
+
+        self.status = data.get("status", status)
+        self.description = data.get(
+            "description", responses.get(status, "Unknown Status Code")
+        )
+        self.message = data.get("message", "")
+
+        super().__init__(f"{self.status} {self.description}: {self.message}")
+
+
+class Forbidden(DiscodoException):
+    """Exception that is thrown when HTTP status code is 403."""
+
+    ...
+
+
+class TooManyRequests(DiscodoException):
+    """Exception that is thrown when HTTP status code is 429."""
+
+    ...
 
 
 class NotSeekable(DiscodoException):
-    pass
+    """Exception that is thrown when trying to seek the source which is not seekable."""
 
+    ...
 
-class NotPlaying(DiscodoException):
-    pass
+
+class NodeException(DiscodoException):
+    """Exception that is thrown when discodo node returns some exception."""
+
+    def __init__(self, name, reason) -> None:
+        self.name = name
+        self.reason = reason
+
+        super().__init__(f"{name}{': ' if reason else ''}{reason}")
 
 
 class NodeNotConnected(DiscodoException):
-    pass
+    """Exception that is thrown when there is no discodo node that is connected."""
+
+    ...
diff --git a/discodo/event.py b/discodo/event.py
deleted file mode 100644
index ee2fa763..00000000
--- a/discodo/event.py
+++ /dev/null
@@ -1,72 +0,0 @@
-import logging
-
-from .utils import EventDispatcher
-from .voice_client import VoiceClient
-
-log = logging.getLogger("discodo.event")
-
-
-class DiscordEvent:
-    dispatcher = EventDispatcher()
-
-    def __init__(self, manager) -> None:
-        self.manager = manager
-
-        self.dispatcher.on("READY", self.READY)
-        self.dispatcher.on("RESUME", self.RESUME)
-        self.dispatcher.on("VOICE_STATE_UPDATE", self.VOICE_STATE_UPDATE)
-        self.dispatcher.on("VOICE_SERVER_UPDATE", self.VOICE_SERVER_UPDATE)
-
-    def dispatch(self, event: str, *args, **kwargs) -> None:
-        return self.dispatcher.dispatch(event, *args, **kwargs)
-
-    async def READY(self, data: dict) -> None:
-        log.info(
-            f'ready event dispatched. set session id {data["session_id"]} and user id {data["user"]["id"]}'
-        )
-        self.manager.session_id = data["session_id"]
-        self.manager.id = int(data["user"]["id"])
-
-    async def RESUME(self, data: dict) -> None:
-        log.info(
-            f'resume event dispatched. set session id {data["session_id"]} and user id {data["user"]["id"]}'
-        )
-        self.manager.session_id = data["session_id"]
-        self.manager.id = int(data["user"]["id"])
-
-    async def VOICE_STATE_UPDATE(self, data: dict) -> None:
-        if not self.manager.id or data["user_id"] != str(self.manager.id):
-            if self.manager.getVC(data["guild_id"], safe=True):
-                vc = self.manager.getVC(data["guild_id"])
-
-                if data.get("channel_id") and int(data["channel_id"]) == vc.channel_id:
-                    vc.speakState = False
-            return
-
-        log.info(f'recieve self voice update. set session id {data["session_id"]}')
-        self.manager.session_id = data["session_id"]
-
-        if self.manager.getVC(data["guild_id"], safe=True):
-            self.manager.getVC(data["guild_id"]).channel_id = (
-                int(data["channel_id"]) if data.get("channel_id") else None
-            )
-        elif data.get("channel_id"):
-            log.info(f'Voice Client of {data["guild_id"]} not found. create new one.')
-
-            vc = self.manager.voiceClients[int(data["guild_id"])] = VoiceClient(
-                self.manager
-            )
-            vc.guild_id = int(data["guild_id"])
-            vc.channel_id = int(data["channel_id"])
-
-    async def VOICE_SERVER_UPDATE(self, data: dict) -> None:
-        if self.manager.getVC(data["guild_id"], safe=True):
-            log.info(f"Voice server update recieved. connect to new endpoint")
-
-            await self.manager.getVC(data["guild_id"]).createSocket(data)
-        else:
-            log.info(f'Voice Client of {data["guild_id"]} not found. create new one.')
-
-            self.manager.voiceClients[int(data["guild_id"])] = VoiceClient(
-                self.manager, data
-            )
diff --git a/discodo/events.py b/discodo/events.py
new file mode 100644
index 00000000..72781943
--- /dev/null
+++ b/discodo/events.py
@@ -0,0 +1,58 @@
+from .utils import EventDispatcher
+from .voice_client import DiscordVoiceClient
+
+
+class DiscordEvent:
+    def __init__(self, manager):
+        self.manager = manager
+        self.dispatcher = EventDispatcher()
+
+        self.dispatcher.on("READY", self.READY)
+        self.dispatcher.on("RESUME", self.RESUME)
+        self.dispatcher.on("VOICE_STATE_UPDATE", self.VOICE_STATE_UPDATE)
+        self.dispatcher.on("VOICE_SERVER_UPDATE", self.VOICE_SERVER_UPDATE)
+
+    def dispatch(self, event, *args, **kwargs):
+        return self.dispatcher.dispatch(event, *args, **kwargs)
+
+    async def READY(self, data):
+        self.manager.id = str(data["user"]["id"])
+        self.manager.session_id = data["session_id"]
+
+    async def RESUME(self, data):
+        self.manager.id = str(data["user"]["id"])
+        self.manager.session_id = data["session_id"]
+
+    async def VOICE_STATE_UPDATE(self, data):
+        if not self.manager.id or str(data["user_id"]) != self.manager.id:
+            if self.manager.getVC(data["guild_id"], safe=True):
+                vc = self.manager.getVC(data["guild_id"])
+
+                if data.get("channel_id") and str(data["channel_id"]) == vc.channel_id:
+                    vc.speakState = False
+            return
+
+        self.manager.session_id = data["session_id"]
+
+        if self.manager.getVC(data["guild_id"], safe=True):
+            self.manager.getVC(data["guild_id"]).channel_id = (
+                str(data["channel_id"]) if data.get("channel_id") else None
+            )
+        elif data.get("channel_id"):
+            vc = self.manager.voiceClients[str(data["guild_id"])] = DiscordVoiceClient(
+                self.manager
+            )
+            vc.guild_id = str(data["guild_id"])
+            vc.channel_id = str(data["channel_id"])
+
+            vc.dispatcher.dispatch("VC_CREATED", id=vc.id)
+
+    async def VOICE_SERVER_UPDATE(self, data):
+        if self.manager.getVC(data["guild_id"], safe=True):
+            await self.manager.getVC(data["guild_id"]).createSocket(data)
+        else:
+            vc = self.manager.voiceClients[str(data["guild_id"])] = DiscordVoiceClient(
+                self.manager, data
+            )
+
+            vc.dispatcher.dispatch("VC_CREATED", id=vc.id)
diff --git a/discodo/extractor/__init__.py b/discodo/extractor/__init__.py
index 368d7aa1..376f3f08 100644
--- a/discodo/extractor/__init__.py
+++ b/discodo/extractor/__init__.py
@@ -1,4 +1,5 @@
 import asyncio
+import contextlib
 import ipaddress
 import re
 import urllib.parse
@@ -6,7 +7,9 @@
 
 import aiohttp
 
+from ..config import Config
 from ..errors import NoSearchResults
+from .local import extract as local_extract
 from .resolver import resolve
 from .youtube import Youtube
 from .youtube_dl import extract as youtube_dl_extract
@@ -23,52 +26,59 @@
 async def extract(
     query: Union[str, list],
     address: Union[ipaddress.IPv4Address, ipaddress.IPv6Address] = None,
-    **kwargs
+    search: bool = False,
+    **kwargs,
 ) -> dict:
-    connector = aiohttp.TCPConnector(local_addr=(str(address), 0)) if address else None
+    async with aiohttp.ClientSession(
+        connector=aiohttp.TCPConnector(local_addr=(str(address), 0))
+        if address
+        else None
+    ) as session:
+        query = await resolve(query, session)
+
+        if isinstance(query, list):
+            Tasks = list(
+                map(
+                    lambda x: asyncio.Task(extract(x, address=address, **kwargs)), query
+                )
+            )
+
+            await asyncio.wait(Tasks)
 
-    query = await resolve(query, connector)
+            Results = []
+            for Task in Tasks:
+                with contextlib.suppress(Exception):
+                    Results.append(Task.result())
 
-    if isinstance(query, list):
-        Tasks = list(
-            map(lambda x: asyncio.Task(extract(x, address=address, **kwargs)), query)
-        )
+            return Results
 
-        await asyncio.wait(Tasks)
+        if "local" in Config.ENABLED_EXT_RESOLVER:
+            localResult = await local_extract(query)
 
-        Results = []
-        for Task in Tasks:
+            if localResult:
+                return localResult
+
+        if not URL_REGEX.match(query):
             try:
-                Results.append(Task.result())
+                searchResult: list = await Youtube.search(query, session)
             except:
                 pass
+            else:
+                if not searchResult:
+                    raise NoSearchResults
 
-        return Results
-
-    if not URL_REGEX.match(query):
-        try:
-            searchResult: list = await Youtube.search(query, connector)
-        except:
-            pass
-        else:
-            if not searchResult:
-                raise NoSearchResults
-
-            return searchResult[0]
-
-    Match = YOUTUBE_PLAYLIST_ID_REGEX.match(query)
-    if Match:
-        if Match.group(1).startswith(("RD", "UL", "PU")):
-            urlInfo = urllib.parse.parse_qs(urllib.parse.urlparse(query).query)
-
-            if urlInfo.get("v") and urlInfo.get("list"):
-                return await Youtube.extract_mix(
-                    urlInfo["v"][0], urlInfo.get("list")[0], connector
-                )
-        else:
-            return await Youtube.extract_playlist(Match.group(1), connector)
+                return searchResult if search else searchResult[0]
 
-    return await youtube_dl_extract(query, address=address, **kwargs)
+        Match = YOUTUBE_PLAYLIST_ID_REGEX.match(query)
+        if Match:
+            if Match.group(1).startswith(("RD", "UL", "PU")):
+                urlInfo = urllib.parse.parse_qs(urllib.parse.urlparse(query).query)
 
+                if urlInfo.get("v") and urlInfo.get("list"):
+                    return await Youtube.extract_mix(
+                        urlInfo["v"][0], urlInfo.get("list")[0], session
+                    )
+            else:
+                return await Youtube.extract_playlist(Match.group(1), session)
 
-search = Youtube.search
+        return await youtube_dl_extract(query, address=address, **kwargs)
diff --git a/discodo/extractor/local.py b/discodo/extractor/local.py
new file mode 100644
index 00000000..dbc037a5
--- /dev/null
+++ b/discodo/extractor/local.py
@@ -0,0 +1,42 @@
+import asyncio
+import os
+
+import av
+
+
+def _extract(query: str):
+    if not (
+        os.path.exists(query) and os.path.isfile(query) and os.access(query, os.R_OK)
+    ):
+        return
+
+    try:
+        container = av.open(query)
+
+        if not container.streams.audio:
+            return
+
+        filename = os.path.split(query)[1]
+        duration = round(container.duration / 1000000, 2)
+    except av.InvalidDataError:
+        return
+    finally:
+        container.close()
+
+    return {
+        "id": os.path.splitext(filename)[0],
+        "title": filename,
+        "url": query,
+        "duration": duration,
+        "is_file": True,
+    }
+
+
+def extract(
+    query: str,
+    loop: asyncio.AbstractEventLoop = None,
+):
+    if not loop:
+        loop = asyncio.get_event_loop()
+
+    return loop.run_in_executor(None, _extract, query)
diff --git a/discodo/extractor/resolver/__init__.py b/discodo/extractor/resolver/__init__.py
index 1614987d..675a4a9f 100644
--- a/discodo/extractor/resolver/__init__.py
+++ b/discodo/extractor/resolver/__init__.py
@@ -8,14 +8,12 @@
 from .vibe import resolve as vibe_resolve
 
 
-async def resolve(
-    query: str, connector: aiohttp.TCPConnector = None
-) -> Union[str, list]:
+async def resolve(query: str, session=None) -> Union[str, list]:
     if "melon" in Config.ENABLED_EXT_RESOLVER:
-        query = await melon_resolve(query, connector)
+        query = await melon_resolve(query, session)
     if "spotify" in Config.ENABLED_EXT_RESOLVER:
         query = await spotify_resolve(query)
     if "vibe" in Config.ENABLED_EXT_RESOLVER:
-        query = await vibe_resolve(query, connector)
+        query = await vibe_resolve(query, session)
 
     return query
diff --git a/discodo/extractor/resolver/melon.py b/discodo/extractor/resolver/melon.py
index f6eded44..9aa0e32c 100644
--- a/discodo/extractor/resolver/melon.py
+++ b/discodo/extractor/resolver/melon.py
@@ -1,7 +1,6 @@
 import re
 from typing import Union
 
-import aiohttp
 from bs4 import BeautifulSoup
 
 MELON_REGEX = re.compile(
@@ -9,15 +8,14 @@
 )
 
 
-async def get_album(url: str, connector: aiohttp.TCPConnector = None) -> list:
-    async with aiohttp.ClientSession(
+async def get_album(url: str, session) -> list:
+    async with session.get(
+        url,
         headers={
             "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"
         },
-        connector=connector,
-    ) as session:
-        async with session.get(url) as resp:
-            Body = await resp.text()
+    ) as resp:
+        Body = await resp.text()
 
     soup = BeautifulSoup(Body, "html.parser")
 
@@ -36,13 +34,11 @@ async def get_album(url: str, connector: aiohttp.TCPConnector = None) -> list:
     ]
 
 
-async def resolve(
-    query: str, connector: aiohttp.TCPConnector = None
-) -> Union[str, list]:
+async def resolve(query: str, session):
     is_album = MELON_REGEX.match(query)
 
     if is_album:
-        Album = await get_album(query, connector)
+        Album = await get_album(query, session)
 
         if len(Album) == 1:
             return Album[0]
diff --git a/discodo/extractor/resolver/vibe.py b/discodo/extractor/resolver/vibe.py
index ababe97a..3b45261c 100644
--- a/discodo/extractor/resolver/vibe.py
+++ b/discodo/extractor/resolver/vibe.py
@@ -1,8 +1,6 @@
 import re
 from typing import Union
 
-import aiohttp
-
 URL_REGEX = re.compile(
     r"(?:https?:\/\/)?(?:www\.)?vibe\.naver\.com/(chart|track|album)/?(\d+)?"
 )
@@ -22,29 +20,28 @@
 }
 
 
-async def getChart(type: str, connector: aiohttp.TCPConnector = None) -> list:
+async def getChart(type: str, session):
     if not type:
         type = "total"
 
-    async with aiohttp.ClientSession(headers=headers, connector=connector) as session:
-        genreMatch = re.match(r"genre-(\w{1,2}\d{3})", type)
+    genreMatch = re.match(r"genre-(\w{1,2}\d{3})", type)
 
-        if genreMatch:
-            URL = (
-                "https://apis.naver.com/vibeWeb/musicapiweb/vibe/v1/chart/track/genres/"
-                + genreMatch.group(1)
-            )
-        else:
-            URL = CHART_TYPE.get(type)
+    if genreMatch:
+        URL = (
+            "https://apis.naver.com/vibeWeb/musicapiweb/vibe/v1/chart/track/genres/"
+            + genreMatch.group(1)
+        )
+    else:
+        URL = CHART_TYPE.get(type)
 
-            if not URL:
-                return
+        if not URL:
+            return
 
-        async with session.get(URL) as resp:
-            if resp.status == 404:
-                return
+    async with session.get(URL, headers=headers) as resp:
+        if resp.status == 404:
+            return
 
-            Data = await resp.json()
+        Data = await resp.json()
 
     return list(
         map(
@@ -54,29 +51,27 @@ async def getChart(type: str, connector: aiohttp.TCPConnector = None) -> list:
     )
 
 
-async def getTrack(id: str, connector: aiohttp.TCPConnector = None) -> list:
-    async with aiohttp.ClientSession(headers=headers, connector=connector) as session:
-        async with session.get(
-            f"https://apis.naver.com/vibeWeb/musicapiweb/track/{id}"
-        ) as resp:
-            if resp.status == 404:
-                return
+async def getTrack(id: str, session):
+    async with session.get(
+        f"https://apis.naver.com/vibeWeb/musicapiweb/track/{id}", headers=headers
+    ) as resp:
+        if resp.status == 404:
+            return
 
-            Data = await resp.json()
+        Data = await resp.json()
 
     Track = Data["response"]["result"]["track"]
     return f"{', '.join(map(lambda y: y['artistName'], Track['artists']))} - {Track['trackTitle']}"
 
 
-async def getAlbum(id: str, connector: aiohttp.TCPConnector = None) -> list:
-    async with aiohttp.ClientSession(headers=headers, connector=connector) as session:
-        async with session.get(
-            f"https://apis.naver.com/vibeWeb/musicapiweb/album/{id}/tracks"
-        ) as resp:
-            if resp.status == 404:
-                return
+async def getAlbum(id: str, session):
+    async with session.get(
+        f"https://apis.naver.com/vibeWeb/musicapiweb/album/{id}/tracks", headers=headers
+    ) as resp:
+        if resp.status == 404:
+            return
 
-            Data = await resp.json()
+        Data = await resp.json()
 
     return list(
         map(
@@ -86,17 +81,17 @@ async def getAlbum(id: str, connector: aiohttp.TCPConnector = None) -> list:
     )
 
 
-async def resolve(
-    query: str, connector: aiohttp.TCPConnector = None
-) -> Union[str, list]:
+async def resolve(query: str, session):
     Match = URL_REGEX.match(query)
 
     if Match:
         if Match.group(1) == "chart":
-            query = await getChart(Match.group(2), connector) or query
+            got_query = await getChart(Match.group(2), session) or query
         elif Match.group(1) == "track":
-            query = await getTrack(Match.group(2), connector) or query
+            got_query = await getTrack(Match.group(2), session) or query
         elif Match.group(2) == "album":
-            query = await getAlbum(Match.group(2), connector) or query
+            got_query = await getAlbum(Match.group(2), session) or query
+        else:
+            got_query = None
 
-    return query
+        return got_query
diff --git a/discodo/extractor/youtube/mix.py b/discodo/extractor/youtube/mix.py
index 4fa7be4e..1b50fa2d 100644
--- a/discodo/extractor/youtube/mix.py
+++ b/discodo/extractor/youtube/mix.py
@@ -1,27 +1,22 @@
 import json
 import logging
 
-import aiohttp
-
 from . import DATA_JSON, YOUTUBE_HEADERS
 
 log = logging.getLogger("discodo.extractor.youtube")
 
 
-async def extract_mix(
-    vId: str, playlistId: str, connector: aiohttp.TCPConnector = None
-) -> dict:
+async def extract_mix(vId: str, playlistId: str, session):
     if not playlistId.startswith(("RD", "UL", "PU")):
         raise TypeError("playlistId is not Youtube Mix id")
 
     log.info(f"Downloading playlist page of {playlistId}")
-    async with aiohttp.ClientSession(
-        headers=YOUTUBE_HEADERS, connector=connector
-    ) as session:
-        async with session.get(
-            f"https://www.youtube.com/watch", params={"v": vId, "list": playlistId}
-        ) as resp:
-            Body: str = await resp.text()
+    async with session.get(
+        "https://www.youtube.com/watch",
+        headers=YOUTUBE_HEADERS,
+        params={"v": vId, "list": playlistId},
+    ) as resp:
+        Body: str = await resp.text()
 
     Search = DATA_JSON.search(Body)
 
diff --git a/discodo/extractor/youtube/playlist.py b/discodo/extractor/youtube/playlist.py
index 1123abc1..a13c1340 100644
--- a/discodo/extractor/youtube/playlist.py
+++ b/discodo/extractor/youtube/playlist.py
@@ -1,116 +1,105 @@
 import json
 import logging
 
-import aiohttp
-
 from ...config import Config
 from . import DATA_JSON, YOUTUBE_HEADERS
 
 log = logging.getLogger("discodo.extractor.youtube")
 
 
-async def extract_playlist(
-    playlistId: str, connector: aiohttp.TCPConnector = None
-) -> dict:
+async def extract_playlist(playlistId: str, session):
     if playlistId.startswith(("RD", "UL", "PU")):
         raise TypeError("playlistId is Youtube Mix id")
 
     log.info(f"Downloading playlist page of {playlistId}")
-    async with aiohttp.ClientSession(
-        headers=YOUTUBE_HEADERS, connector=connector
-    ) as session:
-        async with session.get(
-            f"https://www.youtube.com/playlist", params={"list": playlistId, "hl": "en"}
-        ) as resp:
-            Body: str = await resp.text()
-
-        Search = DATA_JSON.search(Body)
-
-        if not Search:
-            raise ValueError
-
-        Data: dict = json.loads(Search.group(1))
-
-        if Data.get("alerts"):
-            raise Exception(Data["alerts"][0]["alertRenderer"]["text"]["simpleText"])
-
-        firstPlaylistData: list = Data["contents"]["twoColumnBrowseResultsRenderer"][
-            "tabs"
-        ][0]["tabRenderer"]["content"]["sectionListRenderer"]["contents"][0][
-            "itemSectionRenderer"
-        ][
-            "contents"
-        ][
-            0
-        ][
-            "playlistVideoListRenderer"
-        ]
+    async with session.get(
+        "https://www.youtube.com/playlist",
+        headers=YOUTUBE_HEADERS,
+        params={"list": playlistId, "hl": "en"},
+    ) as resp:
+        Body = await resp.text()
 
-        Sources: list = []
+    Search = DATA_JSON.search(Body)
 
-        def extract_playlist(playlistData: dict, name: str = "contents") -> str:
-            trackList: list = playlistData.get(name)
-            if not trackList:
-                return
+    if not Search:
+        raise ValueError
 
-            continuationsTokens: list = []
-
-            def extract(Track: dict) -> dict:
-                if "playlistVideoRenderer" in Track:
-                    Renderer: dict = Track.get("playlistVideoRenderer", {})
-                    shortBylineText: dict = Renderer.get("shortBylineText")
-
-                    if not Renderer.get("isPlayable") or not shortBylineText:
-                        return
-
-                    return {
-                        "id": Renderer["videoId"],
-                        "title": Renderer["title"].get("simpleText")
-                        or Renderer["title"]["runs"][0]["text"],
-                        "webpage_url": "https://youtube.com/watch?v="
-                        + Renderer["videoId"],
-                        "uploader": shortBylineText["runs"][0]["text"],
-                        "duration": Renderer["lengthSeconds"],
-                    }
-                elif "continuationItemRenderer" in Track:
-                    continuationsTokens.append(
-                        Track["continuationItemRenderer"]["continuationEndpoint"][
-                            "continuationCommand"
-                        ]["token"]
-                    )
+    Data: dict = json.loads(Search.group(1))
 
-                    return
-                else:
+    if Data.get("alerts"):
+        raise Exception(Data["alerts"][0]["alertRenderer"]["text"]["simpleText"])
+
+    firstPlaylistData = Data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][0][
+        "tabRenderer"
+    ]["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"][
+        "contents"
+    ][
+        0
+    ][
+        "playlistVideoListRenderer"
+    ]
+
+    Sources: list = []
+
+    def extract_playlist(playlistData: dict, name: str = "contents"):
+        trackList = playlistData.get(name)
+        if not trackList:
+            return
+
+        continuationsTokens: list = []
+
+        def extract(Track: dict):
+            if "playlistVideoRenderer" in Track:
+                Renderer: dict = Track.get("playlistVideoRenderer", {})
+                shortBylineText = Renderer.get("shortBylineText")
+
+                if not Renderer.get("isPlayable") or not shortBylineText:
                     return
 
-            Sources.extend(map(extract, trackList))
+                return {
+                    "id": Renderer["videoId"],
+                    "title": Renderer["title"].get("simpleText")
+                    or Renderer["title"]["runs"][0]["text"],
+                    "webpage_url": "https://youtube.com/watch?v=" + Renderer["videoId"],
+                    "uploader": shortBylineText["runs"][0]["text"],
+                    "duration": Renderer["lengthSeconds"],
+                }
+            if "continuationItemRenderer" in Track:
+                continuationsTokens.append(
+                    Track["continuationItemRenderer"]["continuationEndpoint"][
+                        "continuationCommand"
+                    ]["token"]
+                )
 
-            if not continuationsTokens:
                 return
+            return
 
-            return (
-                "https://www.youtube.com/browse_ajax?continuation="
-                + continuationsTokens[0]
-                + "&ctoken="
-                + continuationsTokens[0]
-                + "&hl=en"
-            )
+        Sources.extend(map(extract, trackList))
 
-        continuations_url: str = extract_playlist(firstPlaylistData)
-        for _ in range(Config.PLAYLIST_PAGE_LIMIT):
-            if not continuations_url:
-                break
+        if not continuationsTokens:
+            return
 
-            log.info(f"Downloading playlist continuation page of {playlistId}")
-            async with session.get(continuations_url) as resp:
-                Body: dict = await resp.json()
+        return (
+            "https://www.youtube.com/browse_ajax?continuation="
+            + continuationsTokens[0]
+            + "&ctoken="
+            + continuationsTokens[0]
+            + "&hl=en"
+        )
 
-            nextPlaylistData: dict = Body[1]["response"]["onResponseReceivedActions"][
-                0
-            ]["appendContinuationItemsAction"]
+    continuations_url = extract_playlist(firstPlaylistData)
+    for _ in range(Config.PLAYLIST_PAGE_LIMIT):
+        if not continuations_url:
+            break
+
+        log.info(f"Downloading playlist continuation page of {playlistId}")
+        async with session.get(continuations_url, headers=YOUTUBE_HEADERS) as resp:
+            Body = await resp.json()
+
+        nextPlaylistData = Body[1]["response"]["onResponseReceivedActions"][0][
+            "appendContinuationItemsAction"
+        ]
 
-            continuations_url: str = extract_playlist(
-                nextPlaylistData, name="continuationItems"
-            )
+        continuations_url = extract_playlist(nextPlaylistData, name="continuationItems")
 
-        return list(filter(None, Sources))
+    return list(filter(None, Sources))
diff --git a/discodo/extractor/youtube/search.py b/discodo/extractor/youtube/search.py
index 14d4b32b..4be66cbc 100644
--- a/discodo/extractor/youtube/search.py
+++ b/discodo/extractor/youtube/search.py
@@ -2,22 +2,19 @@
 import json
 import logging
 
-import aiohttp
-
 from . import DATA_JSON, YOUTUBE_HEADERS
 
 log = logging.getLogger("discodo.extractor.youtube")
 
 
-async def search(Query: str, connector: aiohttp.TCPConnector = None) -> None:
+async def search(Query: str, session):
     log.info(f"Downloading search page of {Query}")
-    async with aiohttp.ClientSession(
-        headers=YOUTUBE_HEADERS, connector=connector
-    ) as session:
-        async with session.get(
-            "https://www.youtube.com/results", params={"search_query": Query}
-        ) as resp:
-            Body: str = await resp.text()
+    async with session.get(
+        "https://www.youtube.com/results",
+        headers=YOUTUBE_HEADERS,
+        params={"search_query": Query},
+    ) as resp:
+        Body: str = await resp.text()
 
     Search = DATA_JSON.search(Body)
 
@@ -26,8 +23,8 @@ async def search(Query: str, connector: aiohttp.TCPConnector = None) -> None:
 
     Data = json.loads(Search.group(1))
 
-    def extract_video(Data: dict) -> dict:
-        Renderer: dict = Data.get("videoRenderer")
+    def extract_video(Data: dict):
+        Renderer = Data.get("videoRenderer")
 
         if not Renderer or not Renderer.get("lengthText"):
             return
@@ -47,7 +44,7 @@ def extract_video(Data: dict) -> dict:
             ),
         }
 
-    Videos: list = list(
+    Videos = list(
         filter(
             None,
             map(
diff --git a/discodo/extractor/youtube/subtitle.py b/discodo/extractor/youtube/subtitle.py
index 03aed4b9..774600cf 100644
--- a/discodo/extractor/youtube/subtitle.py
+++ b/discodo/extractor/youtube/subtitle.py
@@ -6,19 +6,16 @@
 except ImportError:
     from defusedxml import ElementTree
 
-import aiohttp
-
 log = logging.getLogger("discodo.extractor.youtube")
 
 
-async def get_subtitle(videoId: str, connector: aiohttp.TCPConnector = None) -> list:
+async def get_subtitle(videoId: str, session):
     log.info(f"Downloading subtitle page of {videoId}")
-    async with aiohttp.ClientSession(connector=connector) as session:
-        async with session.get(
-            "https://video.google.com/timedtext",
-            params={"hl": "en", "type": "list", "v": videoId},
-        ) as resp:
-            Body: str = await resp.text()
+    async with session.get(
+        "https://video.google.com/timedtext",
+        params={"hl": "en", "type": "list", "v": videoId},
+    ) as resp:
+        Body: str = await resp.text()
 
     Tree = ElementTree.fromstring(Body)
 
diff --git a/discodo/extractor/youtube_dl.py b/discodo/extractor/youtube_dl.py
index ebcd347a..3d0abfbf 100644
--- a/discodo/extractor/youtube_dl.py
+++ b/discodo/extractor/youtube_dl.py
@@ -1,12 +1,15 @@
 import asyncio
 import copy
 import ipaddress
+import logging
 from typing import Coroutine, Union
 
 from youtube_dl import YoutubeDL as YoutubeDLClient
 
 from ..errors import NoSearchResults
 
+log = logging.getLogger("discodo.extractor.youtube_dl")
+
 YTDLOption = {
     "format": "(bestaudio[ext=opus]/bestaudio/best)[protocol!=http_dash_segments]",
     "nocheckcertificate": True,
@@ -16,6 +19,7 @@
     "skip_download": True,
     "writesubtitles": True,
     "noplaylist": True,
+    "logger": log,
 }
 
 
diff --git a/discodo/gateway.py b/discodo/gateway.py
index 9ca570d7..30beb516 100644
--- a/discodo/gateway.py
+++ b/discodo/gateway.py
@@ -1,105 +1,36 @@
 import asyncio
-import concurrent.futures
+import collections
+import contextlib
+import inspect
 import json
 import logging
 import struct
-import threading
 import time
-from collections import deque
-from typing import Dict, Optional
+from typing import Optional
 
 import websockets
+from websockets.client import WebSocketClientProtocol
 
+from .enums import VoicePayload
 from .natives import Cipher
 
 log = logging.getLogger("discodo.gateway")
 
 
-class keepAliver(threading.Thread):
-    def __init__(self, ws, interval: float, *args, **kwargs) -> None:
-        threading.Thread.__init__(self, *args, **kwargs)
-        self.daemon = True
+class VoiceSocket(websockets.client.WebSocketClientProtocol):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        self.keepAliver = None
+        self.heartbeatTimeout = 60.0
 
-        self.ws = ws
-        self.interval: float = interval
-        self.Stopped = threading.Event()
         self.latency: Optional[float] = None
-        self.recent_latencies = deque(maxlen=20)
+        self.recent_latencies = collections.deque(maxlen=20)
 
         self._lastAck = self._lastSend = time.perf_counter()
-        self.timeout: float = ws.heartbeatTimeout
-        self.threadId: int = ws.threadId
-
-    def __del__(self) -> None:
-        self.stop()
-
-    def run(self) -> None:
-        while not self.Stopped.wait(self.interval):
-            if (self._lastAck + self.timeout) < time.perf_counter():
-                Runner = asyncio.run_coroutine_threadsafe(
-                    self.ws.close(4000), self.ws.loop
-                )
-
-                try:
-                    Runner.result()
-                except:
-                    pass
-
-                self.stop()
-                return
-
-            payload: Dict[str, int] = {
-                "op": VoicePayload.HEARTBEAT,
-                "d": int(time.time() * 1000),
-            }
-            Runner = asyncio.run_coroutine_threadsafe(
-                self.ws.sendJson(payload), self.ws.loop
-            )
-            try:
-                totalBlocked: int = 0
-                while True:
-                    try:
-                        Runner.result(10)
-                        break
-                    except concurrent.futures.TimeoutError:
-                        totalBlocked += 10
-                        log.warning(
-                            f"Thread-{self.threadId}: Heartbeat blocked for more than {totalBlocked} seconds."
-                        )
-            except:
-                return self.stop()
-            else:
-                self._lastSend = time.perf_counter()
-
-    def ack(self) -> None:
-        self._lastAck = time.perf_counter()
-        self.latency = self._lastAck - self._lastSend
-        self.recent_latencies.append(self.latency)
-
-    def stop(self) -> None:
-        self.Stopped.set()
-
-
-class VoicePayload:
-    IDENTIFY = 0
-    SELECT_PROTOCOL = 1
-    READY = 2
-    HEARTBEAT = 3
-    SESSION_DESCRIPTION = 4
-    SPEAKING = 5
-    HEARTBEAT_ACK = 6
-    RESUME = 7
-    HELLO = 8
-    RESUMED = 9
-    CLIENT_DISCONNECT = 13
-
-
-class VoiceSocket(websockets.client.WebSocketClientProtocol):
-    def __del__(self) -> None:
-        self.loop.call_soon_threadsafe(lambda: self.loop.create_task(self.close()))
 
     @classmethod
-    async def connect(cls, client, resume=False):
+    async def connect(cls, client, resume=False) -> WebSocketClientProtocol:
         ws = await websockets.connect(
             f"wss://{client.endpoint}/?v=4",
             loop=client.loop,
@@ -108,37 +39,46 @@ async def connect(cls, client, resume=False):
         )
 
         ws.client = client
-        ws.keepAliver = None
-        ws.heartbeatTimeout = 60.0
-        ws.threadId = threading.get_ident()
 
         if not resume:
-            await ws.identify()
+            await ws.identify()  # type: ignore
         else:
-            await ws.resume()
+            await ws.resume()  # type: ignore
 
         return ws
 
-    @property
-    def latency(self) -> Optional[float]:
-        return self.keepAliver.latency if self.keepAliver else None
+    def __del__(self):
+        self.loop.call_soon_threadsafe(lambda: self.loop.create_task(self.close()))
 
     @property
-    def averageLatency(self) -> Optional[float]:
-        if not self.keepAliver:
-            return None
-
-        return sum(self.keepAliver.recent_latencies) / len(
-            self.keepAliver.recent_latencies
-        )
+    def averagedLatency(self) -> Optional[float]:
+        return sum(self.recent_latencies) / len(self.recent_latencies)
 
     async def sendJson(self, data: dict) -> None:
-        log.debug(f"send to websocket {data}")
         await self.send(json.dumps(data))
 
-    async def identify(self) -> None:
+    async def send(self, data):
+        log.debug(f"{self.client.endpoint} < {data}")
+        return await super().send(data)
+
+    async def handleHeartbeat(self):
+        with contextlib.suppress(websockets.ConnectionClosedError):
+            while True:
+                if self._lastAck + self.heartbeatTimeout < time.perf_counter():
+                    await self.close(4000)
+                    return
+
+                await self.sendJson(
+                    {"op": VoicePayload.HEARTBEAT.value, "d": int(time.time() * 1000)}
+                )
+
+                self._lastSend = time.perf_counter()
+
+                await asyncio.sleep(self.heartbeatInterval)
+
+    async def identify(self):
         payload = {
-            "op": VoicePayload.IDENTIFY,
+            "op": VoicePayload.IDENTIFY.value,
             "d": {
                 "server_id": str(self.client.guild_id),
                 "user_id": str(self.client.user_id),
@@ -148,9 +88,9 @@ async def identify(self) -> None:
         }
         await self.sendJson(payload)
 
-    async def resume(self) -> None:
+    async def resume(self):
         payload = {
-            "op": VoicePayload.RESUME,
+            "op": VoicePayload.RESUME.value,
             "d": {
                 "server_id": str(self.client.guild_id),
                 "user_id": str(self.client.user_id),
@@ -160,9 +100,9 @@ async def resume(self) -> None:
         }
         await self.sendJson(payload)
 
-    async def select_protocol(self, ip: str, port: str, mode: str) -> None:
+    async def select_protocol(self, ip: str, port: str, mode: str):
         payload = {
-            "op": VoicePayload.SELECT_PROTOCOL,
+            "op": VoicePayload.SELECT_PROTOCOL.value,
             "d": {
                 "protocol": "udp",
                 "data": {"address": ip, "port": port, "mode": mode},
@@ -170,31 +110,36 @@ async def select_protocol(self, ip: str, port: str, mode: str) -> None:
         }
         await self.sendJson(payload)
 
-    async def speak(self, state: bool = True) -> None:
+    async def speak(self, state: bool = True):
         self.client.speakState = state
 
         payload = {
-            "op": VoicePayload.SPEAKING,
+            "op": VoicePayload.SPEAKING.value,
             "d": {"speaking": int(state), "delay": 0, "ssrc": self.client.ssrc},
         }
         await self.sendJson(payload)
 
-    async def messageRecieved(self, message: dict) -> None:
-        Operation, Data = message["op"], message.get("d")
-        log.debug(f"websocket recieved {Operation}: {Data}")
+    async def messageRecieved(self, message):
+        log.debug(f"{self.client.endpoint} > {message}")
+        Operation, Data = VoicePayload(message["op"]), message.get("d")
 
         if Operation == VoicePayload.READY:
             await self.createConnection(Data)
         elif Operation == VoicePayload.HEARTBEAT_ACK:
-            self.keepAliver.ack()
+            self._lastAck = time.perf_counter()
+            self.latency = self._lastAck - self._lastSend
+            self.recent_latencies.append(self.latency)
         elif Operation == VoicePayload.SESSION_DESCRIPTION:
             await self.loadKey(Data)
         elif Operation == VoicePayload.HELLO:
-            interval = Data["heartbeat_interval"] / 1000.0
-            self.keepAliver = keepAliver(self, min(interval, 5.0))
-            self.keepAliver.start()
+            self.heartbeatInterval = min(Data["heartbeat_interval"] / 1000.0, 5.0)
+
+            if self.keepAliver and not self.keepAliver.done():
+                self.keepAliver.cancel()
 
-    async def createConnection(self, data: Optional[dict]) -> None:
+            self.keepAliver = self.loop.create_task(self.handleHeartbeat())
+
+    async def createConnection(self, data):
         self.client.ssrc = data["ssrc"]
         self.client.endpointIp = data["ip"]
         self.client.endpointPort = data["port"]
@@ -205,34 +150,33 @@ async def createConnection(self, data: Optional[dict]) -> None:
         self.client.socket.sendto(
             packet, (self.client.endpointIp, self.client.endpointPort)
         )
-        _recieved = await self.loop.sock_recv(self.client.socket, 70)
-
-        start, end = 4, _recieved.index(0, 4)
-        self.client.ip = _recieved[start:end].decode("ascii")
-        self.client.port = struct.unpack_from(">H", _recieved, len(_recieved) - 2)[0]
+        received = await self.loop.sock_recv(self.client.socket, 70)
 
-        encryptModes = [Mode for Mode in data["modes"] if Mode in Cipher.available]
-        log.debug(f'recieved encrypt modes {data["modes"]}')
+        start, end = 4, received.index(0, 4)
+        self.client.ip = received[start:end].decode("ascii")
+        self.client.port = struct.unpack_from(">H", received, len(received) - 2)[0]
 
-        encryptMode = encryptModes[0]
-        log.info(f"select encrypt mode {encryptMode}")
+        encryptMode = list(
+            filter(
+                lambda x: x[0] in data["modes"],
+                inspect.getmembers(Cipher, predicate=inspect.isfunction),
+            )
+        )[0][0]
 
         await self.select_protocol(self.client.ip, self.client.port, encryptMode)
 
-    async def loadKey(self, data: Optional[dict]) -> None:
-        log.info("recieved voice secret key.")
-
+    async def loadKey(self, data):
         self.client.speakState = False
 
         self.client.encryptMode = data["mode"]
         self.client.secretKey = data.get("secret_key")
 
-    async def poll(self) -> None:
+    async def poll(self):
         message = await asyncio.wait_for(self.recv(), timeout=30.0)
         await self.messageRecieved(json.loads(message))
 
-    async def close(self, *args, **kwargs) -> None:
-        if self.keepAliver:
-            self.keepAliver.stop()
+    async def close(self, *args, **kwargs):
+        if self.keepAliver and not self.keepAliver.done():
+            self.keepAliver.cancel()
 
         await super().close(*args, **kwargs)
diff --git a/discodo/manager.py b/discodo/manager.py
index ec534dfa..2f046f54 100644
--- a/discodo/manager.py
+++ b/discodo/manager.py
@@ -1,78 +1,37 @@
 import asyncio
 
 from .errors import VoiceClientNotFound
-from .event import DiscordEvent
-from .source import AudioData
+from .events import DiscordEvent
 from .utils import EventDispatcher
-from .voice_client import VoiceClient
 
 
 class ClientManager:
-    def __init__(self, **kwargs) -> None:
+    def __init__(self, **kwargs):
         self.loop = asyncio.get_event_loop()
 
-        self.id = kwargs.get("user_id")
-        self.session_id = kwargs.get("session_id")
+        self.id = str(kwargs.get("user_id"))
+        self.session_id = str(kwargs.get("session_id"))
 
-        self.Context = {}
         self.voiceClients = {}
 
         self.dispatcher = EventDispatcher()
-        self.event = self.dispatcher.event
         self.discordEvent = DiscordEvent(self)
 
-    def __del__(self) -> None:
+    def __del__(self):
         for voiceClient in self.voiceClients.values():
             self.loop.call_soon_threadsafe(voiceClient.__del__)
 
-    def __repr__(self) -> str:
+    def __repr__(self):
         return f"<ClientManager id={self.id} session_id='{self.session_id}' voiceClients={len(self.voiceClients)}>"
 
-    def discordDispatch(self, data: dict) -> None:
-        Event, Data = data["t"], data["d"]
+    def discordDispatch(self, data):
+        return self.discordEvent.dispatch(data["t"], data=data["d"])
 
-        return self.discordEvent.dispatch(Event, data=Data)
-
-    def getVC(self, guildID: int, safe: bool = False) -> VoiceClient:
-        if int(guildID) not in self.voiceClients and not safe:
+    def getVC(self, guildID, safe=False):
+        if str(guildID) not in self.voiceClients and not safe:
             raise VoiceClientNotFound
 
-        return self.voiceClients.get(int(guildID))
+        return self.voiceClients.get(str(guildID))
 
-    def delVC(self, guildID: int) -> None:
+    def delVC(self, guildID: str) -> None:
         self.getVC(guildID).__del__()
-
-    async def getSource(self, Query: str) -> AudioData:
-        return await AudioData.create(Query)
-
-    def putSource(self, guildID: int, *args, **kwargs) -> int:
-        return self.getVC(guildID).putSource(*args, **kwargs)
-
-    async def loadSource(self, guildID: int, *args, **kwargs) -> AudioData:
-        return await self.getVC(guildID).loadSource(*args, **kwargs)
-
-    def skip(self, guildID: int, offset: int) -> None:
-        return self.getVC(guildID).skip(offset)
-
-    async def seek(self, guildID: int, offset: int) -> None:
-        return await self.getVC(guildID).seek(offset)
-
-    def setVolume(self, guildID: int, value: float) -> float:
-        self.getVC(guildID).volume = value
-
-        return self.getVC(guildID).volume
-
-    def setCrossfade(self, guildID: int, value: float) -> float:
-        self.getVC(guildID).crossfade = value
-
-        return self.getVC(guildID).crossfade
-
-    def setGapless(self, guildID: int, value: bool) -> bool:
-        self.getVC(guildID).gapless = value
-
-        return self.getVC(guildID).gapless
-
-    def setAutoplay(self, guildID: int, value: bool) -> bool:
-        self.getVC(guildID).autoplay = value
-
-        return self.getVC(guildID).autoplay
diff --git a/discodo/natives/AudioFifo.py b/discodo/natives/AudioFifo.py
index 61b53dd4..6f38d204 100644
--- a/discodo/natives/AudioFifo.py
+++ b/discodo/natives/AudioFifo.py
@@ -1,5 +1,6 @@
 import logging
 import threading
+import traceback
 
 import av
 
@@ -43,11 +44,7 @@ def write(self, *args, **kwargs) -> None:
         try:
             super().write(*args, **kwargs)
         except:
+            traceback.print_exc()
             log.warning("while writing on fifo, an error occured, ignored.")
 
         self.check_buffer()
-
-    def reset(self) -> None:
-        super().read(
-            samples=max(self.samples - self.SAMPLES_PER_FRAME, 0), partial=True
-        )
diff --git a/discodo/natives/AudioFilter.py b/discodo/natives/AudioFilter.py
index 3ac43381..cfb87e17 100644
--- a/discodo/natives/AudioFilter.py
+++ b/discodo/natives/AudioFilter.py
@@ -44,5 +44,5 @@ def pull(self) -> av.AudioFrame:
 
         try:
             return self.Graph.pull()
-        except av.error.BlockingIOError:
+        except av.BlockingIOError:  # type: ignore
             return None
diff --git a/discodo/natives/encrypt.py b/discodo/natives/encrypt.py
index ad73b5f8..08c115ce 100644
--- a/discodo/natives/encrypt.py
+++ b/discodo/natives/encrypt.py
@@ -1,9 +1,11 @@
 import nacl.secret
+import nacl.utils
 
 
 class Cipher:
     available = ["xsalsa20_poly1305", "xsalsa20_poly1305_suffix"]
 
+    @staticmethod
     def xsalsa20_poly1305(key: list, header: bytearray, data: bytes) -> bytes:
         Box = nacl.secret.SecretBox(bytes(key))
         Nonce = bytearray(24)
@@ -11,6 +13,7 @@ def xsalsa20_poly1305(key: list, header: bytearray, data: bytes) -> bytes:
 
         return header + Box.encrypt(bytes(data), bytes(Nonce)).ciphertext
 
+    @staticmethod
     def xsalsa20_poly1305_suffix(key: list, header: bytearray, data: bytes) -> bytes:
         Box = nacl.secret.SecretBox(bytes(key))
         Nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)
diff --git a/discodo/planner.py b/discodo/planner.py
index 72b2afea..414e65b7 100644
--- a/discodo/planner.py
+++ b/discodo/planner.py
@@ -1,7 +1,3 @@
-"""
-Thx for @Sannoob
-"""
-
 import collections
 import ipaddress
 import random
@@ -19,8 +15,8 @@ def __init__(self, ipBlocks: list, excludeIps: list = []) -> None:
             Union[ipaddress.IPv4Address, ipaddress.IPv6Address], int
         ] = collections.defaultdict(int)
 
-        self.ipBlocks = [ipaddress.ip_network(ipBlock) for ipBlock in ipBlocks]
-        self.excludeIps = [ipaddress.ip_address(excludeIp) for excludeIp in excludeIps]
+        self.ipBlocks = list(map(ipaddress.ip_network, ipBlocks))
+        self.excludeIps = list(map(ipaddress.ip_address, excludeIps))
 
     def mark_failed_address(
         self,
@@ -37,7 +33,7 @@ def unmark_failed_address(
     @staticmethod
     def __get_random(
         ipBlock: Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
-    ) -> str:
+    ) -> Union[ipaddress.IPv4Address, ipaddress.IPv6Address]:
         cidr: int = ipBlock.prefixlen
 
         host_bits: int = (128 if ipBlock.version == 6 else 32) - cidr
@@ -55,7 +51,7 @@ def get(self) -> Union[ipaddress.IPv4Address, ipaddress.IPv6Address]:
         self.usedCount[sortedIpBlocks[0]] += 1
 
         if len(self.failedAddress) >= sum(
-            [ipBlock.num_addresses for ipBlock in self.ipBlocks]
+            map(lambda ipBlock: ipBlock.num_addresses, self.ipBlocks)
         ):
             raise ValueError("No Ips available")
 
diff --git a/discodo/player.py b/discodo/player.py
index e2ccfefd..c20941fd 100644
--- a/discodo/player.py
+++ b/discodo/player.py
@@ -1,62 +1,56 @@
 import asyncio
 import audioop
+import contextlib
+import logging
 import threading
 import time
 import traceback
-from typing import Callable, Union
 
 from .config import Config
 from .errors import NotPlaying
 from .source import AudioData, AudioSource
 
+log = logging.getLogger("discodo.player")
+
 
 class Player(threading.Thread):
-    def __init__(self, client) -> None:
+    def __init__(self, client):
         threading.Thread.__init__(self)
         self.daemon = True
 
         self.client = client
 
-        self._volume = 1.0
-        self._crossfade = 5.0
         self._end = threading.Event()
 
         self._getSourceTask = None
         self._current = self._next = None
+        self._volume = client.volume
 
         self.loops = 0
-        self._crossfadeLoop = 0
+        self.crossfadeLoops = 0
         self._request_dispatched = False
 
-    def __del__(self) -> None:
-        for Source in self.client.Queue:
-            if isinstance(Source, AudioSource):
-                Source.cleanup()
+    def __del__(self):
+        self.stop()
 
     @property
-    def crossfade(self) -> Union[float, None]:
-        return self._crossfade
-
-    @crossfade.setter
-    def crossfade(self, value: float) -> None:
-        self._crossfade = value
+    def crossfade(self):
+        return self.client.crossfade
 
     @property
-    def crossfadeVolume(self) -> Union[float, None]:
-        return (
-            (1.0 / (self.client.crossfade / Config.DELAY))
-            if self.client.crossfade
-            else 1.0
-        )
-
-    def seek(self, offset: int) -> None:
-        if not self.current:
-            raise NotPlaying
+    def crossfadeVolume(self):
+        return (1.0 / (self.crossfade / Config.DELAY)) if self.crossfade else 1.0
 
-        return self.current.seek(offset)
+    def seek(self, offset):
+        if self.current:
+            return self.current.seek(offset)
+        if self.client.Queue:
+            self.client.Queue[0].start_position = offset
+            return
+        raise NotPlaying
 
     @property
-    def current(self) -> AudioSource:
+    def current(self):
         if not self._current:
             if self.next:
                 self._current, self.next = self.next, None
@@ -72,15 +66,18 @@ def current(self) -> AudioSource:
 
         return self._current
 
-    @current.setter
-    def current(self, _: None) -> None:
-        self.client.dispatcher.dispatch("SOURCE_END", source=self._current)
+    def clearCurrent(self):
+        self.client.dispatcher.dispatch("SOURCE_STOP", source=self._current)
 
         self.client.loop.call_soon_threadsafe(self._current.cleanup)
         self._current = None
 
+    @current.setter
+    def current(self, _):
+        return self.clearCurrent()
+
     @property
-    def next(self) -> AudioSource:
+    def next(self):
         is_load_condition = (
             not self._current
             or self._current.skipped
@@ -112,15 +109,15 @@ def next(self) -> AudioSource:
         if isinstance(self._next, AudioData):
             if is_load_condition:
 
-                def setSource(Source: Union[AudioData, AudioSource]) -> None:
-                    if isinstance(Source, AudioData) and Source == self.client.Queue[0]:
+                def setSource(source) -> None:
+                    if isinstance(source, AudioData) and source == self.client.Queue[0]:
                         self._next = None
                         self.client.Queue.pop(0)
                     if (
-                        isinstance(Source, AudioSource)
-                        and Source.AudioData == self.client.Queue[0]
+                        isinstance(source, AudioSource)
+                        and source.AudioData == self.client.Queue[0]
                     ):
-                        self._next = self.client.Queue[0] = Source
+                        self._next = self.client.Queue[0] = source
 
                 self.getSource(self._next, setSource)
             return None
@@ -135,8 +132,7 @@ def setSource(Source: Union[AudioData, AudioSource]) -> None:
 
         return self._next
 
-    @next.setter
-    def next(self, _: None) -> None:
+    def clearNext(self):
         if not self._next:
             return
 
@@ -156,38 +152,42 @@ def next(self, _: None) -> None:
                 self.client.Queue.pop(0)
         self._next = None
 
+    @next.setter
+    def next(self, _):
+        return self.clearNext()
+
     def getSource(self, *args, **kwargs) -> None:
-        if not self._getSourceTask or self._getSourceTask.done():
-            self._getSourceTask = asyncio.run_coroutine_threadsafe(
-                self._getSource(*args, **kwargs), self.client.loop
-            )
+        async def Task(Data, callback):
+            Source = None
 
-    async def _getSource(self, Data: AudioData, callback: Callable) -> None:
-        Source = None
+            try:
+                Source = await Data.source()
 
-        try:
-            Source = await Data.source()
+                Source.volume = 1.0
+                Source.filter = self.client.filter
+            except Exception:
+                log.exception(f"while processing AudioData {Source}, an error occured")
+                self.client.dispatcher.dispatch(
+                    "SOURCE_TRACEBACK", source=Data, traceback=traceback.format_exc()
+                )
 
-            Source.volume = 1.0
-            Source.filter = self.client.filter
-        except Exception:
-            traceback.print_exc()
-            self.client.dispatcher.dispatch(
-                "SOURCE_TRACEBACK", source=Data, traceback=traceback.format_exc()
-            )
+                Source = Data
+            finally:
+                callback(Source)
 
-            Source = Data
-        finally:
-            callback(Source)
+        if not self._getSourceTask or self._getSourceTask.done():
+            self._getSourceTask = asyncio.run_coroutine_threadsafe(
+                Task(*args, **kwargs), self.client.loop
+            )
 
-    def read(self) -> bytes:
+    def read(self):
         if not self.current:
             return
 
         Data = self.current.read()
 
         if not Data or self.current.volume <= 0.0:
-            self._crossfadeLoop = 0
+            self.crossfadeLoops = 0
             self.current = None
 
             return self.read()
@@ -203,8 +203,8 @@ def read(self) -> bytes:
         if is_crossfade_timing and self.next.AudioFifo.is_ready():
             NextData = self.next.read()
             if NextData:
-                self._crossfadeLoop += 1
-                crossfadeVolume = self.crossfadeVolume * self._crossfadeLoop
+                self.crossfadeLoops += 1
+                crossfadeVolume = self.crossfadeVolume * self.crossfadeLoops
 
                 self.current.volume = 1.0 - crossfadeVolume
                 self.next.volume = crossfadeVolume
@@ -224,57 +224,54 @@ def read(self) -> bytes:
 
         return Data
 
-    def speak(self, state: bool) -> None:
+    def speak(self, state) -> None:
         if self.client.speakState == state:
             return
 
         self.client.speakState = state
         asyncio.run_coroutine_threadsafe(self.client.ws.speak(state), self.client.loop)
 
-    def __do_run(self) -> None:
-        self.loops = 0
-        _start = time.perf_counter()
+    def run(self):
+        with contextlib.suppress(Exception):
+            _start = time.perf_counter()
 
-        while not self._end.is_set():
-            try:
-                if not self.client.connectedThreadEvent.is_set():
-                    self.client.connectedThreadEvent.wait()
-                    self.loops = 0
-                    _start = time.perf_counter()
+            while not self._end.is_set():
+                try:
+                    if not self.client.connectedThreadEvent.is_set():
+                        self.client.connectedThreadEvent.wait()
 
-                Data = self.read() if not self.client.paused else None
+                        self.loops = 0
+                        _start = time.perf_counter()
 
-                if self._volume != self.client.volume:
-                    if self._volume < self.client.volume:
-                        self._volume = round(self._volume + 0.01, 3)
-                    if self._volume > self.client.volume:
-                        self._volume = round(self._volume - 0.01, 3)
+                    Data = self.read() if not self.client.paused else None
 
-                self.speak(bool(Data))
+                    if self._volume != self.client.volume:
+                        if self._volume < self.client.volume:
+                            self._volume = round(self._volume + 0.01, 3)
+                        if self._volume > self.client.volume:
+                            self._volume = round(self._volume - 0.01, 3)
 
-                if Data:
-                    if self._volume != 1.0:
-                        Data = audioop.mul(Data, 2, min(self._volume, 2.0))
+                    self.speak(bool(Data))
 
-                    self.client.send(Data)
+                    if Data:
+                        if self._volume != 1.0:
+                            Data = audioop.mul(Data, 2, min(self._volume, 2.0))
 
-                self.loops += 1
-                nextTime = _start + Config.DELAY * self.loops
-                time.sleep(max(0, Config.DELAY + (nextTime - time.perf_counter())))
-            except Exception:
-                traceback.print_exc()
-                self.dispatcher.dispatch(
-                    "PLAYER_TRACEBACK", traceback=traceback.format_exc()
-                )
+                        self.client.send(Data)
+
+                    self.loops += 1
+                    nextTime = _start + Config.DELAY * self.loops
+                    time.sleep(max(0, Config.DELAY + (nextTime - time.perf_counter())))
+                except:
+                    log.exception(
+                        f"on the player of {self.client.guild_id}, an error occured"
+                    )
+                    self.client.dispatcher.dispatch(
+                        "PLAYER_TRACEBACK", traceback=traceback.format_exc()
+                    )
 
-    def run(self) -> None:
-        try:
-            self.__do_run()
-        except Exception:
-            pass
-        finally:
-            self.stop()
+        self.stop()
 
-    def stop(self) -> None:
+    def stop(self):
         self._end.set()
         self.speak(False)
diff --git a/discodo/server/payloads.py b/discodo/server/payloads.py
index 6bbc693c..0b589c35 100644
--- a/discodo/server/payloads.py
+++ b/discodo/server/payloads.py
@@ -1,15 +1,14 @@
 import asyncio
+import functools
 import uuid
 
-from discodo.source import SubtitleSource
-
-from ..errors import NotConnected, NotPlaying
-from ..extractor import search
-from ..source import AudioData
-from ..status import getStatus
+from ..errors import NotPlaying
+from ..source import SubtitleSource
+from ..utils import getStatus
 
 
 def need_manager(func):
+    @functools.wraps(func)
     def wrapper(self, *args, **kwargs):
         if not self.ClientManager:
             payload = {
@@ -24,17 +23,18 @@ def wrapper(self, *args, **kwargs):
 
 
 class WebsocketPayloads:
-    async def GET_STATUS(self, Data: None) -> None:
+    @staticmethod
+    async def GET_STATUS(self, _):
         payload = {"op": "STATUS", "d": getStatus()}
 
         await self.sendJson(payload)
 
-    async def HEARTBEAT(self, Data: int) -> None:
-        payload = {"op": "HEARTBEAT_ACK", "d": Data}
-
-        await self.sendJson(payload)
+    @staticmethod
+    async def HEARTBEAT(self, data):
+        await self.sendJson({"op": "HEARTBEAT_ACK", "d": data})
 
-    async def IDENTIFY(self, Data: dict) -> None:
+    @staticmethod
+    async def IDENTIFY(self, data):
         if self.ClientManager:
             payload = {
                 "op": "IDENTIFY",
@@ -45,271 +45,20 @@ async def IDENTIFY(self, Data: dict) -> None:
                 },
             }
         else:
-            await self.initialize_manager(Data["user_id"])
+            await self.initializeManager(data["user_id"], data.get("shard_id"))
             payload = {"op": "IDENTIFIED", "d": "ClientManager initialized."}
 
         await self.sendJson(payload)
 
+    @staticmethod
     @need_manager
-    async def DISCORD_EVENT(self, Data: dict) -> None:
-        self.ClientManager.discordDispatch(Data)
-
-    @need_manager
-    async def getContext(self, _: dict) -> None:
-        payload = {
-            "op": "getContext",
-            "d": {"context": self.ClientManager.Context},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def setContext(self, Data: dict) -> None:
-        self.ClientManager.Context.update(Data["context"])
-
-        payload = {
-            "op": "setContext",
-            "d": {"context": self.ClientManager.Context},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def getVCContext(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        payload = {
-            "op": "getVCContext",
-            "d": {"context": VoiceClient.Context},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def setVCContext(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        VoiceClient.Context.update(Data["context"])
-
-        payload = {
-            "op": "setVCContext",
-            "d": {"context": VoiceClient.Context},
-        }
-
-        await self.sendJson(payload)
+    async def DISCORD_EVENT(self, data):
+        self.ClientManager.discordDispatch(data)
 
+    @staticmethod
     @need_manager
-    async def getSource(self, Data: dict) -> None:
-        payload = {
-            "op": "getSource",
-            "d": {
-                "source": await AudioData.create(Data["query"]),
-            },
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def searchSources(self, Data: dict) -> None:
-        payload = {
-            "op": "searchSources",
-            "d": {
-                "sources": filter(
-                    lambda Source: AudioData(Source), await search(Data["query"])
-                ),
-            },
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def putSource(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        payload = {
-            "op": "putSource",
-            "d": {
-                "guild_id": Data["guild_id"],
-                "index": VoiceClient.putSource(Data["source"]),
-            },
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def loadSource(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        await VoiceClient.loadSource(Data["query"])
-
-    @need_manager
-    async def setVolume(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        VoiceClient.volume = Data["volume"]
-
-        payload = {
-            "op": "setVolume",
-            "d": {"guild_id": Data["guild_id"], "volume": VoiceClient.volume},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def setCrossfade(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        VoiceClient.crossfade = Data["crossfade"]
-
-        payload = {
-            "op": "setCrossfade",
-            "d": {"guild_id": Data["guild_id"], "crossfade": VoiceClient.crossfade},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def setAutoplay(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        VoiceClient.autoplay = Data["autoplay"]
-
-        payload = {
-            "op": "setAutoplay",
-            "d": {"guild_id": Data["guild_id"], "autoplay": VoiceClient.autoplay},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def setFilter(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        VoiceClient.filter = Data["filter"]
-
-        payload = {
-            "op": "setFilter",
-            "d": {"guild_id": Data["guild_id"], "filter": VoiceClient.filter},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def seek(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        await VoiceClient.seek(Data["offset"])
-
-        payload = {
-            "op": "seek",
-            "d": {"guild_id": Data["guild_id"], "offset": Data["offset"]},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def skip(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        VoiceClient.skip(Data["offset"])
-
-        payload = {
-            "op": "skip",
-            "d": {"guild_id": Data["guild_id"], "remain": len(VoiceClient.Queue)},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def pause(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        VoiceClient.pause()
-
-        payload = {
-            "op": "pause",
-            "d": {"guild_id": Data["guild_id"], "state": VoiceClient.state},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def resume(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        VoiceClient.resume()
-
-        payload = {
-            "op": "resume",
-            "d": {"guild_id": Data["guild_id"], "state": VoiceClient.state},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def shuffle(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        VoiceClient.shuffle()
-
-        payload = {
-            "op": "shuffle",
-            "d": {"guild_id": Data["guild_id"], "entries": VoiceClient.Queue},
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def remove(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        removed = VoiceClient.Queue[Data["index"] - 1]
-        del VoiceClient.Queue[Data["index"] - 1]
-
-        payload = {
-            "op": "remove",
-            "d": {
-                "guild_id": Data["guild_id"],
-                "removed": removed,
-                "entries": VoiceClient.Queue,
-            },
-        }
-
-        await self.sendJson(payload)
-
-    @need_manager
-    async def getState(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
+    async def getState(self, data):
+        VoiceClient = self.ClientManager.getVC(data["guild_id"])
 
         payload = {
             "op": "getState",
@@ -319,9 +68,13 @@ async def getState(self, Data: dict) -> None:
                 "channel_id": VoiceClient.channel_id,
                 "state": VoiceClient.state,
                 "current": VoiceClient.current,
-                "duration": VoiceClient.current.duration,
-                "position": VoiceClient.current.position,
-                "remain": VoiceClient.current.remain,
+                "duration": VoiceClient.current.duration
+                if VoiceClient.current
+                else None,
+                "position": VoiceClient.current.position
+                if VoiceClient.current
+                else None,
+                "remain": VoiceClient.current.remain if VoiceClient.current else None,
                 "remainQueue": len(VoiceClient.Queue),
                 "options": {
                     "autoplay": VoiceClient.autoplay,
@@ -329,70 +82,54 @@ async def getState(self, Data: dict) -> None:
                     "crossfade": VoiceClient.crossfade,
                     "filter": VoiceClient.filter,
                 },
+                "context": VoiceClient.Context,
             },
         }
 
         return await self.sendJson(payload)
 
+    @staticmethod
     @need_manager
-    async def getQueue(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
+    async def getQueue(self, data):
+        VoiceClient = self.ClientManager.getVC(data["guild_id"])
 
         payload = {
             "op": "getQueue",
             "d": {
-                "guild_id": Data["guild_id"],
+                "guild_id": VoiceClient.guild_id,
                 "entries": VoiceClient.Queue,
             },
         }
 
         return await self.sendJson(payload)
 
+    @staticmethod
     @need_manager
-    async def getQueueSource(self, Data: dict) -> None:
-        VoiceClient = self.ClientManager.getVC(Data["guild_id"])
-        if not VoiceClient:
-            raise NotConnected
-
-        payload = {
-            "op": "getQueue",
-            "d": {
-                "guild_id": Data["guild_id"],
-                "source": next(
-                    filter(lambda x: x.tag == Data["tag"], VoiceClient.Queue), None
-                ),
-            },
-        }
-
-        return await self.sendJson(payload)
+    async def VC_DESTROY(self, data):
+        self.ClientManager.delVC(data["guild_id"])
 
+    @staticmethod
     @need_manager
-    async def VC_DESTROY(self, Data: dict) -> None:
-        self.ClientManager.delVC(Data["guild_id"])
-
-    @need_manager
-    async def requestSubtitle(self, Data: dict) -> None:
-        vc = self.ClientManager.getVC(Data["guild_id"])
+    async def requestSubtitle(self, data):
+        vc = self.ClientManager.getVC(data["guild_id"])
 
         if not vc.current:
             raise NotPlaying
 
-        if "url" in Data:
-            url = Data["url"]
+        if "url" in data:
+            url = data["url"]
         else:
-            if not Data["lang"] in vc.current.subtitles:
+            if not data["lang"] in vc.current.subtitles:
                 payload = {
                     "op": "requestSubtitle",
                     "d": {
-                        "guild_id": Data["guild_id"],
-                        "NoSubtitle": f"There is no subtitle in {Data['lang']}",
+                        "guild_id": data["guild_id"],
+                        "NoSubtitle": f"There is no subtitle in {data['lang']}",
                     },
                 }
                 return await self.sendJson(payload)
 
-            url = vc.current.subtitles[Data["lang"]]
+            url = vc.current.subtitles[data["lang"]]
 
         current = vc.current
         _identify_token = str(uuid.uuid4())
@@ -405,55 +142,55 @@ async def requestSubtitle(self, Data: dict) -> None:
         payload = {
             "op": "requestSubtitle",
             "d": {
-                "guild_id": Data["guild_id"],
+                "guild_id": data["guild_id"],
                 "identify": _identify_token,
                 "url": url,
             },
         }
         await self.sendJson(payload)
 
-        Previous = Now = Next = ""
-        Elements = list(Subtitle.TextElements.values())
-        while not Subtitle.is_done and not current.stopped:
-            Element = Subtitle.seek(current.position)
-
-            if Element and Element["markdown"] and Element["markdown"] != Now:
-                if not Element["markdown"].startswith(Now) and not Element[
-                    "markdown"
-                ].endswith(Now):
-                    Previous = Now
-                Now = Element["markdown"]
-
-                NextElements = [
-                    NextElement["markdown"]
-                    for NextElement in Elements[Elements.index(Element) + 1 :]
-                    if NextElement["markdown"] != Now
-                    if not NextElement["markdown"].startswith(Now)
-                    and not NextElement["markdown"].endswith(Now)
-                ]
-
-                Next = NextElements[0] if NextElements else None
-
-                payload = {
-                    "op": "Subtitle",
-                    "d": {
-                        "guild_id": Data["guild_id"],
-                        "identify": _identify_token,
-                        "previous": Previous,
-                        "current": Now,
-                        "next": Next,
-                    },
-                }
-                await self.sendJson(payload)
-
-            await asyncio.sleep(0.1)
+        try:
+            Previous = Now = Next = ""
+            Elements = list(Subtitle.TextElements.values())
+            while not Subtitle.is_done and not current.stopped:
+                Element = Subtitle.seek(current.position)
+
+                if Element and Element["markdown"] and Element["markdown"] != Now:
+                    if not Element["markdown"].startswith(Now) and not Element[
+                        "markdown"
+                    ].endswith(Now):
+                        Previous = Now
+                    Now = Element["markdown"]
+
+                    NextElements = [
+                        NextElement["markdown"]
+                        for NextElement in Elements[Elements.index(Element) + 1 :]
+                        if NextElement["markdown"] != Now
+                        if not NextElement["markdown"].startswith(Now)
+                        and not NextElement["markdown"].endswith(Now)
+                    ]
+
+                    Next = NextElements[0] if NextElements else None
+
+                    payload = {
+                        "op": "Subtitle",
+                        "d": {
+                            "guild_id": data["guild_id"],
+                            "identify": _identify_token,
+                            "previous": Previous,
+                            "current": Now,
+                            "next": Next,
+                        },
+                    }
+                    await self.sendJson(payload)
 
-        payload = {
-            "op": "subtitleDone",
-            "d": {
-                "guild_id": Data["guild_id"],
-                "identify": _identify_token,
-                "language": Data["language"],
-            },
-        }
-        return await self.sendJson(payload)
+                await asyncio.sleep(0.1)
+        finally:
+            payload = {
+                "op": "subtitleDone",
+                "d": {
+                    "guild_id": data["guild_id"],
+                    "identify": _identify_token,
+                },
+            }
+            return await self.sendJson(payload)
diff --git a/discodo/server/planner.py b/discodo/server/planner.py
index 23a9b3da..bf979523 100644
--- a/discodo/server/planner.py
+++ b/discodo/server/planner.py
@@ -1,5 +1,5 @@
+import functools
 import ipaddress
-from typing import Coroutine
 
 from sanic import Blueprint, response
 from sanic.exceptions import abort
@@ -9,8 +9,9 @@
 app = Blueprint(__name__)
 
 
-def authorized(func: Coroutine) -> Coroutine:
-    def wrapper(request, *args, **kwargs) -> Coroutine:
+def authorized(func):
+    @functools.wraps(func)
+    def wrapper(request, *args, **kwargs):
         if request.headers.get("Authorization") != Config.PASSWORD:
             abort(403, "Password mismatch.")
 
@@ -21,7 +22,7 @@ def wrapper(request, *args, **kwargs) -> Coroutine:
 
 @app.get("/planner")
 @authorized
-async def plannerStatus(request) -> response.json:
+async def plannerStatus(request):
     if not Config.RoutePlanner:
         abort(404, "RoutePlanner is not enabled.")
 
@@ -45,7 +46,7 @@ async def plannerStatus(request) -> response.json:
 
 @app.post("/planner/unmark")
 @authorized
-async def plannerUnmark(request) -> response.json:
+async def plannerUnmark(request):
     if not Config.RoutePlanner:
         abort(404, "RoutePlanner is not enabled.")
 
@@ -60,7 +61,7 @@ async def plannerUnmark(request) -> response.json:
 
 @app.post("/planner/unmark/all")
 @authorized
-async def plannerUnmark(request) -> response.json:
+async def plannerUnmarkAll(request):
     if not Config.RoutePlanner:
         abort(404, "RoutePlanner is not enabled.")
 
diff --git a/discodo/server/restful.py b/discodo/server/restful.py
index d82a014f..7f6d70cd 100644
--- a/discodo/server/restful.py
+++ b/discodo/server/restful.py
@@ -1,18 +1,18 @@
+import functools
 import json
-from typing import Coroutine
 
 from sanic import Blueprint, response
 from sanic.exceptions import abort
 
 from ..config import Config
-from ..extractor import search
 from ..source import AudioData
 
 app = Blueprint(__name__)
 
 
-def authorized(func: Coroutine) -> Coroutine:
-    def wrapper(request, *args, **kwargs) -> Coroutine:
+def authorized(func):
+    @functools.wraps(func)
+    def wrapper(request, *args, **kwargs):
         if request.headers.get("Authorization") != Config.PASSWORD:
             abort(403, "Password mismatch.")
 
@@ -21,36 +21,46 @@ def wrapper(request, *args, **kwargs) -> Coroutine:
     return wrapper
 
 
-def need_manager(func: Coroutine) -> Coroutine:
+def need_manager(func):
+    @functools.wraps(func)
     def wrapper(request, *args, **kwargs):
-        user_id = int(request.headers.get("User-ID"))
+        tag = str(request.headers.get("User-ID")) + (
+            f"-{request.headers.get('Shard-ID')}"
+            if request.headers.get("Shard-ID") is not None
+            else ""
+        )
 
         if (
-            not hasattr(request.app, "ClientManagers")
-            or not user_id in request.app.ClientManagers
+            not hasattr(request.app.ctx, "ClientManagers")
+            or not tag in request.app.ctx.ClientManagers
         ):
             abort(404, "ClientManager not found.")
 
-        manager = request.app.ClientManagers[user_id]
+        manager = request.app.ctx.ClientManagers[tag]
 
         return func(request, manager, *args, **kwargs)
 
     return wrapper
 
 
-def need_voiceclient(func: Coroutine) -> Coroutine:
+def need_voiceclient(func):
+    @functools.wraps(func)
     def wrapper(request, *args, **kwargs):
-        user_id = int(request.headers.get("User-ID"))
+        tag = str(request.headers.get("User-ID")) + (
+            f"-{request.headers.get('Shard-ID')}"
+            if request.headers.get("Shard-ID") is not None
+            else ""
+        )
 
         if (
-            not hasattr(request.app, "ClientManagers")
-            or not user_id in request.app.ClientManagers
+            not hasattr(request.app.ctx, "ClientManagers")
+            or not tag in request.app.ctx.ClientManagers
         ):
             abort(404, "ClientManager not found.")
 
-        manager = request.app.ClientManagers[user_id]
+        manager = request.app.ctx.ClientManagers[tag]
 
-        guild_id = int(request.headers.get("Guild-ID"))
+        guild_id = str(request.headers.get("Guild-ID"))
 
         VoiceClient = manager.getVC(guild_id, safe=True)
 
@@ -68,8 +78,9 @@ def wrapper(request, *args, **kwargs):
 
 
 class Encoder(json.JSONEncoder):
-    def default(self, o):
-        return o.__dict__()
+    @staticmethod
+    def default(o):
+        return o.toDict()
 
 
 def JSONResponse(
@@ -87,47 +98,9 @@ def JSONResponse(
     )
 
 
-@app.get("/getContext")
-@authorized
-@need_manager
-async def getContext(request, manager) -> JSONResponse:
-    return JSONResponse({"context": manager.Context})
-
-
-@app.get("/setContext")
-@authorized
-@need_manager
-async def setContext(request, manager) -> JSONResponse:
-    if "context" not in request.json:
-        abort(400, "Bad data `context`")
-
-    manager.Context.update(request.json["context"])
-
-    return JSONResponse({"context": manager.Context})
-
-
-@app.get("/getVCContext")
-@authorized
-@need_voiceclient
-async def getVCContext(request, VoiceClient) -> JSONResponse:
-    return JSONResponse({"context": VoiceClient.Context})
-
-
-@app.get("/setVCContext")
-@authorized
-@need_voiceclient
-async def setVCContext(request, VoiceClient) -> JSONResponse:
-    if "context" not in request.json:
-        abort(400, "Bad data `context`")
-
-    VoiceClient.Context.update(request.json["context"])
-
-    return JSONResponse({"context": VoiceClient.Context})
-
-
 @app.get("/getSource")
 @authorized
-async def getSource(request) -> JSONResponse:
+async def getSource(request):
     Query = "".join(request.args.get("query", [])).strip()
     if not Query:
         abort(400, "Missing parameter query.")
@@ -137,32 +110,51 @@ async def getSource(request) -> JSONResponse:
 
 @app.get("/searchSources")
 @authorized
-async def searchSources(request) -> JSONResponse:
+async def searchSources(request):
     Query = "".join(request.args.get("query", [])).strip()
     if not Query:
         abort(400, "Missing parameter query.")
 
-    return JSONResponse(
-        {"sources": filter(lambda Source: AudioData(Source), await search(Query))}
-    )
+    return JSONResponse({"sources": await AudioData.create(Query, search=True)})
+
+
+@app.get("/context")
+@authorized
+@need_voiceclient
+async def getContext(request, VoiceClient):
+    return JSONResponse(VoiceClient.Context)
+
+
+@app.post("/context")
+@authorized
+@need_voiceclient
+async def setContext(request, VoiceClient):
+    VoiceClient.Context = request.json["context"]
+
+    return JSONResponse(VoiceClient.Context)
 
 
 @app.post("/putSource")
 @authorized
 @need_voiceclient
-async def putSource(request, VoiceClient) -> JSONResponse:
+async def putSource(request, VoiceClient):
     if "source" not in request.json:
         abort(400, "Bad data `source`")
 
-    index = VoiceClient.putSource(request.json["source"])
+    source = (
+        list(map(AudioData, request.json["source"]))
+        if isinstance(request.json["source"], list)
+        else AudioData(request.json["source"])
+    )
+    VoiceClient.putSource(source)
 
-    return JSONResponse({"index": index})
+    return JSONResponse({"source": source})
 
 
 @app.post("/loadSource")
 @authorized
 @need_voiceclient
-async def loadSource(request, VoiceClient) -> JSONResponse:
+async def loadSource(request, VoiceClient):
     if "query" not in request.json:
         abort(400, "Bad data `query`")
 
@@ -171,61 +163,64 @@ async def loadSource(request, VoiceClient) -> JSONResponse:
     return JSONResponse({"source": Source})
 
 
-@app.post("/setVolume")
+@app.get("/options")
 @authorized
 @need_voiceclient
-async def setVolume(request, VoiceClient) -> response.empty:
-    if "volume" not in request.json or not isinstance(request.json["volume"], float):
-        abort(400, "Bad data `volume`")
-
-    VoiceClient.volume = request.json["volume"]
-
-    return response.empty()
+async def getOptions(request, VoiceClient):
+    return JSONResponse(
+        {
+            "autoplay": VoiceClient.autoplay,
+            "volume": VoiceClient.volume,
+            "crossfade": VoiceClient.crossfade,
+            "filter": VoiceClient.filter,
+        }
+    )
 
 
-@app.post("/setCrossfade")
+@app.post("/options")
 @authorized
 @need_voiceclient
-async def setCrossfade(request, VoiceClient) -> response.empty:
-    if "crossfade" not in request.json or not isinstance(
-        request.json["crossfade"], float
-    ):
-        abort(400, "Bad data `crossfade`")
+async def setOptions(request, VoiceClient):
+    if "volume" in request.json:
+        VoiceClient.volume = request.json["volume"]
 
-    VoiceClient.crossfade = request.json["crossfade"]
-
-    return response.empty()
+    if "crossfade" in request.json:
+        VoiceClient.crossfade = request.json["crossfade"]
 
+    if "autoplay" in request.json:
+        VoiceClient.autoplay = request.json["autoplay"]
 
-@app.post("/setAutoplay")
-@authorized
-@need_voiceclient
-async def setAutoplay(request, VoiceClient) -> response.empty:
-    if "autoplay" not in request.json or not isinstance(request.json["autoplay"], bool):
-        abort(400, "Bad data `autoplay`")
-
-    VoiceClient.autoplay = request.json["autoplay"]
+    if "filter" in request.json:
+        VoiceClient.filter = request.json["filter"]
 
-    return response.empty()
+    return JSONResponse(
+        {
+            "autoplay": VoiceClient.autoplay,
+            "volume": VoiceClient.volume,
+            "crossfade": VoiceClient.crossfade,
+            "filter": VoiceClient.filter,
+        }
+    )
 
 
-@app.post("/setFilter")
+@app.get("/seek")
 @authorized
 @need_voiceclient
-async def setFilter(request, VoiceClient) -> response.empty:
-    if "filter" not in request.json or not isinstance(request.json["filter"], dict):
-        abort(400, "Bad data `filter`")
-
-    VoiceClient.filter = request.json["filter"]
-
-    return response.empty()
+async def getSeek(request, VoiceClient):
+    return JSONResponse(
+        {
+            "duration": VoiceClient.current.duration,
+            "position": VoiceClient.current.position,
+            "remain": VoiceClient.current.remain,
+        }
+    )
 
 
 @app.post("/seek")
 @authorized
 @need_voiceclient
-async def seek(request, VoiceClient) -> response.empty:
-    if "offset" not in request.json or not isinstance(request.json["offset"], float):
+async def seek(request, VoiceClient):
+    if "offset" not in request.json:
         abort(400, "Bad data `offset`")
 
     await VoiceClient.seek(request.json["offset"])
@@ -236,19 +231,18 @@ async def seek(request, VoiceClient) -> response.empty:
 @app.post("/skip")
 @authorized
 @need_voiceclient
-async def skip(request, VoiceClient) -> JSONResponse:
-    if "offset" not in request.json or not isinstance(request.json["offset"], int):
-        abort(400, "Bad data `offset`")
+async def skip(request, VoiceClient):
+    offset = request.json.get("offset", 1)
 
-    VoiceClient.skip(request.json["offset"])
+    VoiceClient.skip(offset)
 
-    return JSONResponse({"remain": len(VoiceClient.Queue)})
+    return response.empty()
 
 
 @app.post("/pause")
 @authorized
 @need_voiceclient
-async def pause(request, VoiceClient) -> response.empty:
+async def pause(request, VoiceClient):
     VoiceClient.pause()
 
     return response.empty()
@@ -257,7 +251,7 @@ async def pause(request, VoiceClient) -> response.empty:
 @app.post("/resume")
 @authorized
 @need_voiceclient
-async def resume(request, VoiceClient) -> response.empty:
+async def resume(request, VoiceClient):
     VoiceClient.resume()
 
     return response.empty()
@@ -266,56 +260,115 @@ async def resume(request, VoiceClient) -> response.empty:
 @app.post("/shuffle")
 @authorized
 @need_voiceclient
-async def shuffle(request, VoiceClient) -> JSONResponse:
+async def shuffle(request, VoiceClient):
     VoiceClient.shuffle()
 
     return JSONResponse({"entries": VoiceClient.Queue})
 
 
-@app.post("/remove")
+@app.get("/current")
 @authorized
 @need_voiceclient
-async def remove(request, VoiceClient) -> JSONResponse:
-    if "index" not in request.json or not isinstance(request.json["index"], int):
-        abort(400, "Bad data `index`")
-
-    removed = VoiceClient.Queue[request.json["index"] - 1]
-    del VoiceClient.Queue[request.json["index"] - 1]
-
-    return JSONResponse({"removed": removed, "entries": VoiceClient.Queue})
+async def current(request, VoiceClient):
+    return JSONResponse(VoiceClient.current)
 
 
-@app.get("/state")
+@app.get("/queue")
 @authorized
 @need_voiceclient
-async def state(request, VoiceClient) -> JSONResponse:
+async def queue(request, VoiceClient):
     return JSONResponse(
         {
-            "id": VoiceClient.id,
-            "guild_id": VoiceClient.guild_id,
-            "channel_id": VoiceClient.channel_id,
-            "state": VoiceClient.state,
-            "current": VoiceClient.current,
-            "duration": VoiceClient.current.duration,
-            "position": VoiceClient.current.position,
-            "remain": VoiceClient.current.remain,
-            "remainQueue": len(VoiceClient.Queue),
-            "options": {
-                "autoplay": VoiceClient.autoplay,
-                "volume": VoiceClient.volume,
-                "crossfade": VoiceClient.crossfade,
-                "filter": VoiceClient.filter,
-            },
+            "entries": VoiceClient.Queue,
         }
     )
 
 
-@app.get("/queue")
+@app.get("/queue/<index:int>")
 @authorized
 @need_voiceclient
-async def queue(request, VoiceClient) -> JSONResponse:
-    return JSONResponse(
-        {
-            "entries": VoiceClient.Queue,
-        }
-    )
+async def queueItem(request, VoiceClient, index):
+    return JSONResponse(VoiceClient.Queue[index])
+
+
+@app.get("/queue/<tag:string>")
+@authorized
+@need_voiceclient
+async def queueItemTag(request, VoiceClient, tag):
+    searched = list(filter(lambda x: x.tag == tag, VoiceClient.Queue))
+
+    return JSONResponse(searched.pop(0))
+
+
+@app.post("/current")
+@authorized
+@need_voiceclient
+async def changeCurrent(request, VoiceClient):
+    source = VoiceClient.current
+
+    if "context" in request.json:
+        source.Context = request.json["context"]
+
+    return JSONResponse(source)
+
+
+@app.post("/queue/<index:int>")
+@authorized
+@need_voiceclient
+async def changeQueueItem(request, VoiceClient, index):
+    source = VoiceClient.Queue[index]
+
+    if "index" in request.json:
+        source = VoiceClient.Queue.pop(index)
+        VoiceClient.Queue.insert(index, source)
+
+    if "context" in request.json:
+        source.Context = request.json["context"]
+
+    if "start_position" in request.json:
+        source.start_position = request.json["start_position"]
+
+    return JSONResponse(source)
+
+
+@app.post("/queue/<tag:string>")
+@authorized
+@need_voiceclient
+async def changeQueueItemTag(request, VoiceClient, tag):
+    searched = list(filter(lambda x: x.tag == tag, VoiceClient.Queue))
+    source, index = searched[0], VoiceClient.Queue.index(searched[0])
+
+    if "index" in request.json:
+        source = VoiceClient.Queue.pop(index)
+        VoiceClient.Queue.insert(index, source)
+
+    if "context" in request.json:
+        source.Context = request.json["context"]
+
+    if "start_position" in request.json:
+        source.start_position = request.json["start_position"]
+
+    return JSONResponse(source)
+
+
+@app.delete("/queue/<index:int>")
+@authorized
+@need_voiceclient
+async def removeQueueItem(request, VoiceClient, index):
+    removed = VoiceClient.Queue[index]
+    del VoiceClient.Queue[index]
+
+    return JSONResponse({"removed": removed, "entries": VoiceClient.Queue})
+
+
+@app.delete("/queue/<tag:string>")
+@authorized
+@need_voiceclient
+async def removeQueueItemTag(request, VoiceClient, tag):
+    searched = list(filter(lambda x: x.tag == tag, VoiceClient.Queue))
+    index = VoiceClient.Queue.index(searched[0])
+
+    removed = VoiceClient.Queue[index]
+    del VoiceClient.Queue[index]
+
+    return JSONResponse({"removed": removed, "entries": VoiceClient.Queue})
diff --git a/discodo/server/server.py b/discodo/server/server.py
index d34b96b1..26fd2ab9 100644
--- a/discodo/server/server.py
+++ b/discodo/server/server.py
@@ -1,37 +1,26 @@
-from typing import Coroutine
-
 from sanic import Sanic, response
-from sanic.exceptions import abort
 
 from .. import __version__
-from ..config import Config
-from ..status import getStatus
+from ..utils import getStatus
 from .planner import app as PlannerBlueprint
 from .restful import app as RestfulBlueprint
 from .websocket import app as WebsocketBlueprint
 
-app = Sanic(__name__)
+app = Sanic(__name__, configure_logging=False)
+
+app.config.KEEP_ALIVE_TIMEOUT = 75
+app.config.FALLBACK_ERROR_FORMAT = "json"
 
-app.blueprint(WebsocketBlueprint)
 app.blueprint(PlannerBlueprint)
+app.blueprint(WebsocketBlueprint)
 app.blueprint(RestfulBlueprint)
 
 
-def authorized(func: Coroutine) -> Coroutine:
-    def wrapper(request, *args, **kwargs) -> Coroutine:
-        if request.headers.get("Authorization") != Config.PASSWORD:
-            abort(403, "Password mismatch.")
-
-        return func(request, *args, **kwargs)
-
-    return wrapper
-
-
 @app.route("/")
-async def index(request) -> response:
+async def index(request):
     return response.html(f"<h1>Discodo</h1> <h3>{__version__}")
 
 
-@app.get("/status")
-async def status(request) -> response:
+@app.route("/status")
+async def status(request):
     return response.json(getStatus())
diff --git a/discodo/server/websocket.py b/discodo/server/websocket.py
index 1043398e..b164d281 100644
--- a/discodo/server/websocket.py
+++ b/discodo/server/websocket.py
@@ -1,7 +1,8 @@
 import asyncio
+import contextlib
 import json
 import logging
-from typing import Coroutine
+import uuid
 
 from sanic import Blueprint
 from sanic.websocket import ConnectionClosed
@@ -11,52 +12,62 @@
 from ..manager import ClientManager
 from .payloads import WebsocketPayloads
 
-log = logging.getLogger("discodo.server")
-
 app = Blueprint(__name__)
 
+log = logging.getLogger("discodo.server.websocket")
+
 
 @app.websocket("/ws")
-async def socket_feed(request, ws) -> None:
+async def feedSocket(request, ws) -> None:
     handler = WebsocketHandler(request, ws)
     await handler.join()
 
 
 class Encoder(json.JSONEncoder):
-    def default(self, o):
-        return o.__dict__()
+    @staticmethod
+    def default(o):
+        return o.toDict()
 
 
 class ModifiedClientManager(ClientManager):
-    def __init__(self, *args, **kwargs) -> None:
+    def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
         self._binded = asyncio.Event()
 
 
 class WebsocketHandler:
-    def __init__(self, request, ws) -> None:
+    def __init__(self, request, ws):
+        self.state = str(uuid.uuid4())
         self.loop = asyncio.get_event_loop()
 
-        self.request = request
         self.ws = ws
+        self.request = request
         self.app = request.app
 
-        if not hasattr(self.app, "ClientManagers"):
-            self.app.ClientManagers = {}
+        if not hasattr(self.app.ctx, "ClientManagers"):
+            self.app.ctx.ClientManagers = {}
 
         self.ClientManager = None
 
         self._running = asyncio.Event()
-        self.loop.create_task(self._handle())
+        self.loop.create_task(self.handle())
 
-    def __del__(self) -> None:
+    def __del__(self):
         if self.ClientManager:
-            self.ClientManager.dispatcher.offAny(self.manager_event)
+            if self.managerEvent in self.ClientManager.dispatcher._Any:
+                self.ClientManager.dispatcher.offAny(self.managerEvent)
 
-            self.loop.create_task(self.wait_for_bind())
+            self.loop.create_task(self.waitForBind())
+
+    @property
+    def requester(self):
+        return f"{self.request.ip}:{self.request.port}"
+
+    async def waitForBind(self):
+        if self.state != self.ClientManager.state:
+            return
 
-    async def wait_for_bind(self) -> None:
         self.ClientManager._binded.clear()
 
         try:
@@ -65,54 +76,62 @@ async def wait_for_bind(self) -> None:
             )
         except asyncio.TimeoutError:
             self.ClientManager.__del__()
-            del self.app.ClientManagers[int(self.ClientManager.id)]
+            del self.app.ctx.ClientManagers[self.ClientManager.tag]
             self.ClientManager = None
 
-    def join(self) -> Coroutine:
+    def join(self):
         return self._running.wait()
 
-    async def _handle(self) -> None:
-        log.info(f"new websocket connection created from {self.request.ip}.")
+    async def sendJson(self, data):
+        log.debug(f"{self.requester} < {data}")
 
-        if self.request.headers.get("Authorization") != Config.PASSWORD:
-            log.warning(
-                f"websocket connection from {self.request.ip} forbidden: password mismatch."
-            )
-            return await self.forbidden("Password mismatch.")
+        with contextlib.suppress(ConnectionClosed):
+            await self.ws.send(json.dumps(data, cls=Encoder))
 
-        await self.hello()
+    async def handle(self):
+        if self.request.headers.get("Authorization") != Config.PASSWORD:
+            return await self.sendJson({"op": "FORBIDDEN", "d": "Password mismatch."})
+
+        await self.sendJson(
+            {
+                "op": "HELLO",
+                "d": {
+                    "version": __version__,
+                    "heartbeat_interval": Config.HANDSHAKE_INTERVAL,
+                },
+            }
+        )
 
         while True:
             try:
                 RAWDATA = await asyncio.wait_for(
                     self.ws.recv(), timeout=Config.HANDSHAKE_TIMEOUT
                 )
-            except (asyncio.TimeoutError, ConnectionClosed) as exception:
-                if isinstance(exception, asyncio.TimeoutError):
-                    log.info("websocket connection closing because of timeout.")
-                elif isinstance(exception, ConnectionClosed):
-                    log.info(
-                        f"websocket connection disconnected. code {exception.code}"
-                    )
-
+            except (asyncio.TimeoutError, ConnectionClosed) as e:
+                if self.managerEvent in self.ClientManager.dispatcher._Any:
+                    self.ClientManager.dispatcher.offAny(self.managerEvent)
                 return
 
             try:
-                _Data = json.loads(RAWDATA)
-                Operation, Data = _Data.get("op"), _Data.get("d")
+                _data = json.loads(RAWDATA)
+                log.debug(f"{self.requester} > {_data}")
+
+                Operation, Data = _data.get("op"), _data.get("d")
             except:
                 continue
 
             if Operation and hasattr(WebsocketPayloads, Operation):
-                log.debug(f"{Operation} dispatched with {Data}")
-
                 Func = getattr(WebsocketPayloads, Operation)
                 self.loop.create_task(self.__run_event(Func, Operation, Data))
 
-    async def __run_event(self, Func: Coroutine, Operation: str, Data) -> None:
+    async def __run_event(self, Func, Operation, Data):
         try:
             await Func(self, Data)
         except Exception as e:
+            log.exception(
+                f"while handle {Func.__name__} on {self.requester}, an error occured."
+            )
+
             payload = {
                 "op": Operation,
                 "d": {"traceback": {type(e).__name__: str(e)}},
@@ -123,53 +142,26 @@ async def __run_event(self, Func: Coroutine, Operation: str, Data) -> None:
 
             await self.sendJson(payload)
 
-    async def sendJson(self, Data: dict) -> None:
-        log.debug(f"send {Data} to websocket connection of {self.request.ip}.")
-        try:
-            await self.ws.send(json.dumps(Data, cls=Encoder))
-        except ConnectionClosed:
-            log.debug(
-                f"while sending data to websocket connection of {self.request.ip}, the connection closed, ignored."
-            )
+    async def initializeManager(self, user_id, shard_id=None) -> None:
+        tag = f"{user_id}{f'-{shard_id}' if shard_id is not None else ''}"
 
-    async def initialize_manager(self, user_id: int) -> None:
-        if int(user_id) in self.app.ClientManagers:
-            self.ClientManager = self.app.ClientManagers[int(user_id)]
+        if tag in self.app.ctx.ClientManagers:
+            self.ClientManager = self.app.ctx.ClientManagers[tag]
             self.ClientManager._binded.set()
 
             self.loop.create_task(self.resumed())
-            log.debug(f"ClientManager of {user_id} resumed.")
         else:
             self.ClientManager = ModifiedClientManager(user_id=user_id)
-            self.app.ClientManagers[int(user_id)] = self.ClientManager
-
-            log.debug(f"ClientManager of {user_id} intalized.")
-
-        self.ClientManager.dispatcher.onAny(self.manager_event)
-
-    async def manager_event(self, guild_id: int, **kwargs) -> None:
-        Event = kwargs.pop("event")
+            self.ClientManager.tag = tag
+            self.app.ctx.ClientManagers[tag] = self.ClientManager
 
-        payload = {"op": Event, "d": {"guild_id": guild_id}}
-        payload["d"] = dict(payload["d"], **kwargs)
+        self.ClientManager.state = self.state
+        self.ClientManager.dispatcher.onAny(self.managerEvent)
 
-        await self.sendJson(payload)
-
-    async def hello(self) -> None:
-        payload = {
-            "op": "HELLO",
-            "d": {
-                "version": __version__,
-                "heartbeat_interval": Config.HANDSHAKE_INTERVAL,
-            },
-        }
-
-        await self.sendJson(payload)
-
-    async def forbidden(self, message: str) -> None:
-        payload = {"op": "FORBIDDEN", "d": message}
-
-        await self.sendJson(payload)
+    async def managerEvent(self, guild_id, **kwargs) -> None:
+        await self.sendJson(
+            {"op": kwargs.pop("event"), "d": {"guild_id": guild_id, **kwargs}}
+        )
 
     async def resumed(self) -> None:
         payload = {
diff --git a/discodo/source/AudioData.py b/discodo/source/AudioData.py
index f48e94dd..ea9bf579 100644
--- a/discodo/source/AudioData.py
+++ b/discodo/source/AudioData.py
@@ -1,5 +1,5 @@
 import uuid
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
 
 import aiohttp
 import yarl
@@ -13,56 +13,87 @@
 
 
 class AudioData:
-    def __init__(self, data: dict) -> None:
-        self.tag = str(uuid.uuid4())
-
-        self.id = data["id"]
-        self.title = data.get("title")
-
-        if data.get("_type") == "url" and data.get("ie_key") == "Youtube":
-            self.webpage_url = f"https://www.youtube.com/watch?v={self.id}"
-            self.thumbnail = f"https://i.ytimg.com/vi/{self.id}/hqdefault.jpg"
-            self.stream_url = None
-        else:
-            self.webpage_url = data.get("webpage_url")
-            self.thumbnail = data.get("thumbnail")
-            self.stream_url = data.get("url")
-
-        self.duration = data.get("duration")
-        self.is_live = data.get("is_live", False)
-
-        self.uploader = data.get("uploader")
-        self.description = data.get("description")
-
-        def getUsableSubtitle(Data: Dict[str, Any]) -> Dict[str, Any]:
-            Usable: List[Any] = list(
-                filter(lambda Subtitle: Subtitle.get("ext") == "srv1", Data)
+    _source = None
+
+    def __init__(self, data, context=None) -> None:
+        if data.get("_type") in ["AudioData", "AudioSource"]:
+            self.address = Config.RoutePlanner.get() if Config.RoutePlanner else None
+
+            self.tag = data["tag"]
+            self.id = data["id"]
+            self.title = data["title"]
+            self.webpage_url = data["webpage_url"]
+            self.thumbnail = data["thumbnail"]
+            self.stream_url = data["url"]
+            self.duration = data["duration"]
+            self.is_live = data["is_live"]
+            self.is_file = data["is_file"]
+            self.uploader = data["uploader"]
+            self.description = data["description"]
+            self.subtitles = data["subtitles"]
+            self.chapters = data["chapters"]
+            self.related = data["related"]
+            self.Context = data["context"]
+
+            self.start_position = (
+                data["position"]
+                if data["_type"] == "AudioSource"
+                else data["start_position"]
             )
+        else:
+            self.tag = str(uuid.uuid4())
+            self.start_position = 0.0
+
+            self.id = data["id"]
+            self.title = data.get("title")
+
+            if data.get("_type") == "url" and data.get("ie_key") == "Youtube":
+                self.webpage_url = f"https://www.youtube.com/watch?v={self.id}"
+                self.thumbnail = f"https://i.ytimg.com/vi/{self.id}/hqdefault.jpg"
+                self.stream_url = None
+            else:
+                self.webpage_url = data.get("webpage_url")
+                self.thumbnail = data.get("thumbnail")
+                self.stream_url = data.get("url")
+
+            self.duration = data.get("duration")
+            self.is_live = data.get("is_live", False)
+            self.is_file = data.get("is_file", False)
+
+            self.uploader = data.get("uploader")
+            self.description = data.get("description")
+
+            def getUsableSubtitle(Data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
+                Usable: List[Any] = list(
+                    filter(lambda Subtitle: Subtitle.get("ext") == "srv1", Data)
+                )
 
-            if Usable:
-                return Usable[0]["url"]
-
-        self.subtitles = (
-            dict(
-                map(
-                    lambda Data: (
-                        Data[0],
-                        getUsableSubtitle(Data[1]),
-                    ),
-                    data["subtitles"].items(),
+                if Usable:
+                    return Usable[0]["url"]
+
+            self.subtitles = (
+                dict(
+                    map(
+                        lambda Data: (
+                            Data[0],
+                            getUsableSubtitle(Data[1]),
+                        ),
+                        data["subtitles"].items(),
+                    )
                 )
+                if "subtitles" in data
+                else {}
             )
-            if "subtitles" in data
-            else {}
-        )
 
-        self.chapters = data.get("chapters", {})
+            self.chapters = data.get("chapters", {})
 
-        self.related: bool = False
+            self.related: bool = False
+            self.Context = {}
 
-        self._source = None
+        if context:
+            self.Context = context
 
-    def __dict__(self) -> dict:
+    def toDict(self) -> dict:
         return {
             "_type": "AudioData",
             "tag": self.tag,
@@ -73,22 +104,25 @@ def __dict__(self) -> dict:
             "url": self.stream_url,
             "duration": self.duration,
             "is_live": self.is_live,
+            "is_file": self.is_file,
             "uploader": self.uploader,
             "description": self.description,
             "subtitles": self.subtitles,
             "chapters": self.chapters,
             "related": self.related,
+            "context": self.Context,
+            "start_position": self.start_position,
         }
 
     def __repr__(self) -> str:
         return f"<AudioData id={self.id} title='{self.title}' duration={self.duration} address='{self.address}'>"
 
     @classmethod
-    async def create(cls, query: str):
+    async def create(cls, query: str, search=False):
         cls.address = Config.RoutePlanner.get() if Config.RoutePlanner else None
 
         try:
-            Data = await extract(query, address=cls.address)
+            Data = await extract(query, address=cls.address, search=search)
         except youtube_dl.utils.DownloadError as exc:
             if Config.RoutePlanner and exc.exc_info[1].status == 429:
                 Config.RoutePlanner.mark_failed_address(cls.address)
@@ -114,7 +148,7 @@ async def gather(self):
 
             raise exc
 
-        self.__init__(Data)
+        self.__init__(Data, context=self.Context)
 
         return self
 
@@ -125,32 +159,38 @@ async def source(
             await self.gather()
 
         if not self._source:
-            async with aiohttp.ClientSession() as session:
-                URL = yarl.URL(self.stream_url, encoded=True)
-                async with session.get(URL) as resp:
-                    Status = resp.status
-
-            if Status == 403:
-                if _retry == 0:
-                    return await self.source(
-                        *args, _retry=_retry + 1, _limited=True, **kwargs
-                    )
-                elif _retry == 1:
-                    await clear_cache()
-                    return await self.source(
-                        *args, _retry=_retry + 1, _limited=True, **kwargs
-                    )
-
-                raise Forbidden
-            if Status == 429:
-                if Config.RoutePlanner:
-                    Config.RoutePlanner.mark_failed_address(self.address)
-                    self.address = Config.RoutePlanner.get()
-
-                    return await self.source(*args, _limited=True, **kwargs)
-
-                raise TooManyRequests
-
-            self._source = AudioSource(self.stream_url, AudioData=self, **kwargs)
+            if not self.is_file:
+                async with aiohttp.ClientSession() as session:
+                    URL = yarl.URL(self.stream_url, encoded=True)
+                    async with session.get(URL) as resp:
+                        Status = resp.status
+
+                if Status == 403:
+                    if _retry == 0:
+                        return await self.source(
+                            *args, _retry=_retry + 1, _limited=True, **kwargs
+                        )
+                    if _retry == 1:
+                        await clear_cache()
+                        return await self.source(
+                            *args, _retry=_retry + 1, _limited=True, **kwargs
+                        )
+
+                    raise Forbidden
+                if Status == 429:
+                    if Config.RoutePlanner:
+                        Config.RoutePlanner.mark_failed_address(self.address)
+                        self.address = Config.RoutePlanner.get()
+
+                        return await self.source(*args, _limited=True, **kwargs)
+
+                    raise TooManyRequests
+
+            self._source = AudioSource(
+                self.stream_url,
+                AudioData=self,
+                start_position=self.start_position,
+                **kwargs,
+            )
 
         return self._source
diff --git a/discodo/source/AudioSource.py b/discodo/source/AudioSource.py
index dcef4a7c..dce7eb55 100644
--- a/discodo/source/AudioSource.py
+++ b/discodo/source/AudioSource.py
@@ -1,4 +1,5 @@
-from typing import Any, Coroutine
+import time
+from typing import Any, Coroutine, NoReturn
 
 from ..errors import NotSeekable
 from .PyAVSource import PyAVSource
@@ -11,19 +12,26 @@ def __init__(self, *args, AudioData=None, **kwargs) -> None:
         self.AudioData = AudioData
         self.address = self.AudioData.address if self.AudioData else None
 
+        self.Context = AudioData.Context
         self._skipped: bool = False
 
-    def __dict__(self) -> dict:
-        Value = self.AudioData.__dict__() if self.AudioData else {}
+    def toDict(self) -> dict:
+        Value = self.AudioData.toDict() if self.AudioData else {}
 
         Value["_type"] = "AudioSource"
         Value["seekable"] = self.seekable
         Value["duration"] = self.duration
+        Value["context"] = self.Context
+        Value["as_of"] = time.time()
         if hasattr(self, "position"):
             Value["position"] = self.position
 
         return Value
 
+    @classmethod
+    def fromDict(cls, data):
+        raise TypeError("AudioSource cannot be made from dictionary.")
+
     def __repr__(self) -> str:
         return (
             f"<AudioSource id={self.id} title='{self.title}' duration={self.duration}"
@@ -38,7 +46,7 @@ def __getattr__(self, key: str) -> Any:
     def seekable(self) -> bool:
         return not self.AudioData.is_live if self.AudioData else True
 
-    def seek(self, *args, **kwargs) -> Coroutine:
+    def seek(self, *args, **kwargs):
         if not self.seekable:
             raise NotSeekable
 
@@ -57,7 +65,7 @@ def filter(self) -> dict:
         return self._filter
 
     @filter.setter
-    def filter(self, value: dict) -> dict:
+    def filter(self, value: dict) -> None:
         if self.AudioData and self.AudioData.is_live and "atempo" in value:
             raise ValueError("Cannot use `atempo` filter in live streaming.")
 
diff --git a/discodo/source/PyAVSource.py b/discodo/source/PyAVSource.py
index c0fb3690..fc245e02 100644
--- a/discodo/source/PyAVSource.py
+++ b/discodo/source/PyAVSource.py
@@ -20,10 +20,12 @@
 
 
 class PyAVSource:
-    def __init__(self, Source: str) -> None:
+    def __init__(self, Source: str, start_position: float = 0.0) -> None:
         self.loop = asyncio.get_event_loop()
 
         self.Source: str = Source
+        self.start_position: float = start_position
+
         self.AVOption: dict = AVOption
         self.Container: av.StreamContainer = None
         self.selectAudioStream = self.FrameGenerator = None
@@ -33,6 +35,7 @@ def __init__(self, Source: str) -> None:
         self._waitforread = threading.Lock()
         self._loading = threading.Lock()
         self._seeking = threading.Lock()
+        self._seeked = False
         self.BufferLoader: Loader = None
 
         self.AudioFifo = AudioFifo()
@@ -103,6 +106,8 @@ def read(self) -> bytes:
 
     def _seek(self, offset: float, *args, **kwargs) -> None:
         with withLock(self._seeking):
+            self._seeked = True
+
             if not self.Container:
                 if not self._loading.locked():
                     self.Container = av.open(
@@ -168,6 +173,11 @@ def _do_run(self) -> None:
                 )
             self.Source._duration = round(self.Source.Container.duration / 1000000, 2)
 
+            if self.Source.start_position:
+                self.Source.Container.seek(
+                    round(max(self.Source.start_position, 1) * 1000000), any_frame=True
+                )
+
             self.Source.selectAudioStream = self.Source.Container.streams.audio[0]
             self.Source.FrameGenerator = self.Source.Container.decode(
                 self.Source.selectAudioStream
@@ -201,7 +211,11 @@ def _do_run(self) -> None:
 
                 if _seek_locked:
                     self.Source._seeking.release()
-                    self.Source.AudioFifo.reset()
+
+                if self.Source._seeked:
+                    self.Source._seeked = False
+
+                    self.Source.AudioFifo = AudioFifo()
 
                 if not Frame:
                     self.Source.stop()
diff --git a/discodo/source/SubtitleSource.py b/discodo/source/SubtitleSource.py
index ffc32be8..58b63ea8 100644
--- a/discodo/source/SubtitleSource.py
+++ b/discodo/source/SubtitleSource.py
@@ -52,14 +52,14 @@ def __init__(self, Body: str) -> None:
         }
 
         self.duration = sorted(
-            [TextElement["end"] for TextElement in self.TextElements.values()]
+            map(lambda TextElement: TextElement["end"], self.TextElements.values())
         )[-1]
 
     @classmethod
     async def load(cls, URL: str):
         async with aiohttp.ClientSession() as session:
-            async with session.get(URL) as session:
-                Data = await session.text()
+            async with session.get(URL) as resp:
+                Data = await resp.text()
 
         # Blocking is suspicious
         return cls(Data)
@@ -88,13 +88,13 @@ def __init__(self, Data: str):
         }
 
         self.duration = sorted(
-            [TextElement["end"] for TextElement in self.TextElements.values()]
+            map(lambda TextElement: TextElement["end"], self.TextElements.values())
         )[-1]
 
     @classmethod
     async def load(cls, URL: str):
         async with aiohttp.ClientSession() as session:
-            async with session.get(URL) as session:
-                Data = await session.text()
+            async with session.get(URL) as resp:
+                Data = await resp.text()
 
         return cls(Data)
diff --git a/discodo/source/abc.py b/discodo/source/abc.py
index d692ecef..16d1a01f 100644
--- a/discodo/source/abc.py
+++ b/discodo/source/abc.py
@@ -22,7 +22,7 @@ def seek(self, time: float) -> str:
 
         return self.lyrics
 
-    def __dict__(self) -> dict:
+    def toDict(self) -> dict:
         return self.TextElements
 
     @property
diff --git a/discodo/utils/__init__.py b/discodo/utils/__init__.py
index 96933315..847f3bab 100644
--- a/discodo/utils/__init__.py
+++ b/discodo/utils/__init__.py
@@ -1,2 +1,4 @@
 from . import tcp, threadLock
+from .callbackList import CallbackList
 from .eventDispatcher import EventDispatcher
+from .status import *
diff --git a/discodo/utils/callbackList.py b/discodo/utils/callbackList.py
new file mode 100644
index 00000000..b3d1610b
--- /dev/null
+++ b/discodo/utils/callbackList.py
@@ -0,0 +1,47 @@
+class CallbackList(list):
+    """A list that has callback when the value is changed."""
+
+    CALLBACK_METHOD = [
+        "append",
+        "extend",
+        "insert",
+        "remove",
+        "pop",
+        "reverse",
+        "clear",
+    ]
+
+    @staticmethod
+    def callback(name, *args):
+        """Override this function to use callback"""
+
+        ...
+
+    def __setitem__(self, index: int, value):
+        self.callback("setItem", index, value)
+
+        return super().__setitem__(index, value)
+
+    def __delitem__(self, index: int):
+        self.callback("delItem", index)
+
+        return super().__delitem__(index)
+
+    def __iadd__(self, value):
+        self.callback("extend", value)
+
+        return super().__iadd__(value)
+
+    def __getattribute__(self, name: str):
+        results = super().__getattribute__(name)
+
+        if callable(results) and results.__name__ in self.CALLBACK_METHOD:
+
+            def wrapper(*args):
+                self.callback(name, *args)
+
+                return results(*args)
+
+            return wrapper
+
+        return results
diff --git a/discodo/utils/eventDispatcher.py b/discodo/utils/eventDispatcher.py
index aecf7b84..97246d93 100644
--- a/discodo/utils/eventDispatcher.py
+++ b/discodo/utils/eventDispatcher.py
@@ -2,10 +2,16 @@
 import collections
 import functools
 import traceback
-from typing import Callable, Coroutine
+from typing import Callable
 
 
 class EventDispatcher:
+    r"""Represents an event dispatcher similar to EventEmitter_
+
+    .. _EventEmitter: https://nodejs.org/api/events.html#events_class_eventemitter
+
+    :var Optional[asyncio.AbstractEventLoop] loop: The event loop that the dispatcher uses for operation, defaults to :py:meth:`asyncio.get_event_loop`"""
+
     def __init__(self, loop: asyncio.AbstractEventLoop = None) -> None:
         self._Events = collections.defaultdict(list)
         self._listeners = collections.defaultdict(list)
@@ -14,22 +20,54 @@ def __init__(self, loop: asyncio.AbstractEventLoop = None) -> None:
         self.loop = loop or asyncio.get_event_loop()
 
     def on(self, event: str, func: Callable):
+        """Adds the ``func`` function to the end of the listeners list of the ``event``
+
+        :param str event: The event name to listen to.
+        :param Callable func: The function to call when event dispatching.
+
+        :rtype: discodo.EventDispatcher"""
+
         self._Events[event].append(func)
         return self
 
     def off(self, event: str, func: Callable):
+        """Remove the ``func`` function from the listeners list of the ``event``
+
+        :param str event: The event name to remove from.
+        :param Callable func: The function to remove from the list.
+
+        :rtype: discodo.EventDispatcher"""
+
         self._Events[event].remove(func)
         return self
 
     def onAny(self, func: Callable):
+        """Adds the ``func`` function to the end of the listeners list
+
+        :param Callable func: The function to call when event dispatching.
+
+        :rtype: discodo.EventDispatcher"""
+
         self._Any.append(func)
         return self
 
     def offAny(self, func: Callable):
+        """Remove the ``func`` function from the listeners list
+
+        :param Callable func: The function to remove from the list.
+
+        :rtype: discodo.EventDispatcher"""
+
         self._Any.remove(func)
         return self
 
     def dispatch(self, event_: str, *args, **kwargs) -> None:
+        r"""Call the listeners which is matched with event name.
+
+        :param str event_: The event name to dispatch.
+        :param \*args: An argument list of data to dispatch with.
+        :param \*kwargs: A keyword argument list of data to dispatch with."""
+
         if self._Any:
             for func in self._Any:
                 try:
@@ -78,21 +116,28 @@ def dispatch(self, event_: str, *args, **kwargs) -> None:
                 traceback.print_exc()
 
     def event(self, event: str):
+        """A decorator that registers an event to listen to.
+
+        :param str event: The event name to listen to."""
+
         def wrapper(func):
             self.on(event, func)
 
         return wrapper
 
-    def once(self, event: str, func: Callable):
-        def wrapper(*args, **kwargs):
-            self.off(event, func)
-            return event(*args, **kwargs)
+    async def wait_for(
+        self, event: str, condition: Callable = None, timeout: float = None
+    ):
+        """Waits for an event that is matching with ``condition`` to be dispatched for ``timeout``
 
-        return self.on(event, wrapper)
+        :param str event: The event name to wait for
+        :param Optional[Callable] condition: A predicate to check what to wait for. this function must return ``bool``
+        :param Optional[float] timeout: The number of seconds to wait.
+
+        :raises asyncio.TimeoutError: The timeout is provided and it was reached.
+
+        :rtype: Any"""
 
-    def wait_for(
-        self, event: str, condition: Callable = None, timeout: float = None
-    ) -> Coroutine:
         Future = self.loop.create_future()
 
         if not condition:
@@ -100,4 +145,4 @@ def wait_for(
 
         self._listeners[event].append((Future, condition))
 
-        return asyncio.wait_for(Future, timeout)
+        return await asyncio.wait_for(Future, timeout)
diff --git a/discodo/status.py b/discodo/utils/status.py
similarity index 52%
rename from discodo/status.py
rename to discodo/utils/status.py
index 81f7714d..7681d8b0 100644
--- a/discodo/status.py
+++ b/discodo/utils/status.py
@@ -7,38 +7,74 @@
 
 
 def getProcessMemory():
+    """Get process memory usage. the unit is mega bytes.
+
+    :rtype: int"""
+
     return round(Process.memory_info().rss / 1e6)
 
 
 def getProcessCpu():
+    """Get process cpu usage. the unit is percent.
+
+    :rtype: int"""
+
     return Process.cpu_percent()
 
 
 def getProcessThreads():
+    """Get process thread count.
+
+    :rtype: int"""
+
     return threading.active_count()
 
 
 def getTotalCpu():
+    """Get system cpu usage. the unit is percent.
+
+    :rtype: int"""
+
     return psutil.cpu_percent()
 
 
 def getMemory():
+    """Get system memory usage. the unit is mega bytes.
+
+    :rtype: int"""
+
     return round(psutil.virtual_memory().total / 1e6)
 
 
 def getCpuCount():
+    """Get cpu core count.
+
+    :rtype: int"""
+
     return psutil.cpu_count()
 
 
 def getNetworkInbound():
-    return round(psutil.net_io_counters().bytes_recv / 1e6)
+    """Get network inbound counters. the unit is mega bytes.
+
+    :rtype: int"""
+
+    return round(psutil.net_io_counters().bytes_recv / 1e6)  # type: ignore
 
 
 def getNetworkOutbound():
-    return round(psutil.net_io_counters().bytes_sent / 1e6)
+    """Get network outbound counters. the unit is mega bytes.
+
+    :rtype: int"""
+
+    return round(psutil.net_io_counters().bytes_sent / 1e6)  # type: ignore
 
 
 def getStatus():
+    """Represents all information to dictionary
+
+    :rtype: dict"""
+
     return {
         "UsedMemory": getProcessMemory(),
         "TotalMemory": getMemory(),
diff --git a/discodo/utils/tcp.py b/discodo/utils/tcp.py
index d8afb7d7..47cfca71 100644
--- a/discodo/utils/tcp.py
+++ b/discodo/utils/tcp.py
@@ -2,7 +2,11 @@
 from contextlib import closing
 
 
-def getFreePort():
+def getFreePort() -> int:
+    """Get free port from the system.
+
+    :rtype: int"""
+
     with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
         sock.bind(("", 0))
         _, port = sock.getsockname()
diff --git a/discodo/voice_client.py b/discodo/voice_client.py
index a6b5e87a..59d984f6 100644
--- a/discodo/voice_client.py
+++ b/discodo/voice_client.py
@@ -1,54 +1,46 @@
-import logging
 import random
 import re
-from typing import Union
 
 from youtube_related import RateLimited
 from youtube_related import preventDuplication as relatedClient
 
 from .config import Config
 from .connector import VoiceConnector
+from .enums import PlayerState
 from .errors import NotPlaying
 from .player import Player
 from .source import AudioData, AudioSource
-from .utils import EventDispatcher
+from .utils import CallbackList, EventDispatcher
 
-log = logging.getLogger("discodo.VoiceClient")
 
-YOUTUBE_VIDEO_REGEX = re.compile(
-    r"^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$"
-)
-
-
-class VoiceClient(VoiceConnector):
-    def __init__(self, *args, **kwargs) -> None:
-        super().__init__(*args, **kwargs)
+class DiscordVoiceClient(VoiceConnector):
+    def __init__(self, manager, data=None):
+        super().__init__(manager, data=data)
 
         self.relatedClient = relatedClient()
 
         self.dispatcher = EventDispatcher()
-        self.dispatcher.onAny(self.__dispatchToManager)
+        self.dispatcher.onAny(
+            lambda event, *args, **kwargs: manager.dispatcher.dispatch(
+                self.guild_id, *args, event=event, **kwargs
+            )
+        )
         self.dispatcher.on("REQUIRE_NEXT_SOURCE", self.__fetchAutoPlay)
 
         self.Context = {}
+        self.Queue = CallbackList()
+        self.Queue.callback = self.__queueCallback
 
-        self.Queue = []
         self.player = None
-
-        self._filter = {}
         self.paused = False
 
-        self._autoplay = Config.DEFAULT_AUTOPLAY
-        self._crossfade = Config.DEFAULT_CROSSFADE
-
+        self.filter = {}
+        self.autoplay = Config.DEFAULT_AUTOPLAY
         self._volume = Config.DEFAULT_VOLUME
+        self._crossfade = Config.DEFAULT_CROSSFADE
 
-        self.dispatcher.dispatch("VC_CREATED", id=self.id)
-
-    def __del__(self) -> None:
-        guild_id = int(self.guild_id) if self.guild_id else None
-
-        log.info(f"destroying voice client of {guild_id}.")
+    def __del__(self):
+        guild_id = self.guild_id if self.guild_id else None
 
         if self.manager.voiceClients.get(guild_id) == self:
             self.dispatcher.dispatch("VC_DESTROYED")
@@ -59,260 +51,130 @@ def __del__(self) -> None:
         if self.player and self.player.is_alive():
             self.player.stop()
 
-        for Item in self.Queue:
-            if isinstance(Item, AudioSource):
-                self.loop.call_soon_threadsafe(Item.cleanup)
+        for Item in filter(lambda x: isinstance(x, AudioSource), self.Queue):
+            self.loop.call_soon_threadsafe(Item.cleanup)
 
     def __repr__(self) -> str:
         return f"<VoiceClient guild_id={self.guild_id} volume={self.volume} crossfade={self.crossfade} autoplay={self.autoplay}>"
 
-    def __dispatchToManager(self, event, *args, **kwargs) -> None:
-        self.manager.dispatcher.dispatch(self.guild_id, *args, event=event, **kwargs)
-
     async def __fetchAutoPlay(self, current, **_):
-        for _ in range(5):
-            if self.autoplay and not self.Queue:
-                if YOUTUBE_VIDEO_REGEX.match(current.webpage_url):
-                    address = Config.RoutePlanner.get() if Config.RoutePlanner else None
-                    try:
-                        Related = await self.relatedClient.async_get(
-                            current.webpage_url, local_addr=address
-                        )
-                    except RateLimited:
-                        Config.RoutePlanner.mark_failed_address(address)
-                    else:
-                        return await self.loadSource(
-                            "https://www.youtube.com/watch?v=" + Related["id"],
-                            related=True,
-                        )
-
-    def __spawnPlayer(self) -> None:
-        if self.player and self.player.is_alive():
-            return
-
-        self.player = Player(self)
-        self.player.crossfade = self.crossfade
+        if (
+            self.autoplay
+            and not self.Queue
+            and re.match(
+                r"^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$",
+                current.webpage_url,
+            )
+        ):
+            for _ in range(5):
+                address = Config.RoutePlanner.get() if Config.RoutePlanner else None
+                try:
+                    Related = await self.relatedClient.async_get(
+                        current.webpage_url, local_addr=address
+                    )
+                except RateLimited:
+                    Config.RoutePlanner.mark_failed_address(address)
+                else:
+                    return await self.loadSource(
+                        "https://www.youtube.com/watch?v=" + Related["id"],
+                        related=True,
+                    )
 
-        self.player.start()
+    def __queueCallback(self, name, *args):
+        self.dispatcher.dispatch("QUEUE_EVENT", name=name, args=args)
 
     @property
-    def channel_id(self) -> int:
+    def channel_id(self):
         return self._channel_id
 
     @channel_id.setter
-    def channel_id(self, value: int) -> None:
-        self._channel_id = int(value)
+    def channel_id(self, value) -> None:
+        self._channel_id = value
 
         self.dispatcher.dispatch("VC_CHANNEL_EDITED", channel_id=self._channel_id)
 
-    async def createSocket(self, data: dict = None) -> None:
-        """
-        Create UDP socket with discord to send voice packet.
+    async def createSocket(self, data=None):
+        await super().createSocket(data=data)
 
-        :param dict data: A VOICE_SERVER_UPDATE payload, defaults to None
-        :rtype: None
-        """
-
-        await super().createSocket(data)
+        if self.player and self.player.is_alive():
+            return
 
-        self.__spawnPlayer()
+        self.player = Player(self)
+        self.player.start()
 
     @property
-    def state(self) -> str:
-        """
-        The current state of playback of the voice client
-
-        :getter: Returns VoiceClient state
-        :rtype: str
-        """
-
+    def state(self):
         if not self.player:
-            return "disconnected"
+            return PlayerState.DISCONNECTED
         elif not self.Queue and not self.player.current:
-            return "stopped"
+            return PlayerState.STOPPED
         elif self.paused:
-            return "paused"
+            return PlayerState.PAUSED
         else:
-            return "playing"
+            return PlayerState.PLAYING
 
     @property
     def volume(self) -> float:
-        """
-        Current volume of the voice client (0.0~2.0)
-
-        :getter: Returns VoiceClient volume
-        :setter: Set VoiceClient volume
-        :rtype: float
-        """
-
         return self._volume
 
     @volume.setter
-    def volume(self, value: float) -> None:
+    def volume(self, value: float):
         self._volume = round(max(value, 0.0), 2)
 
     @property
     def crossfade(self) -> float:
-        """
-        Current crossfade value of the voice client
-
-        :getter: Returns VoiceClient crossfade
-        :setter: Set VoiceClient crossfade
-        :rtype: float
-        """
-
         return self._crossfade
 
     @crossfade.setter
-    def crossfade(self, value: float) -> None:
-        if not isinstance(value, float):
-            return TypeError("`crossfade` property must be `float`.")
-
-        self.player.crossfade = self._crossfade = round(max(value, 0.0), 1)
+    def crossfade(self, value: float):
+        self._crossfade = round(max(value, 0.0), 1)
 
     @property
-    def autoplay(self) -> bool:
-        """
-        Current autoplay value of the voice client
-
-        :getter: Returns VoiceClient autoplay
-        :setter: Set VoiceClient autoplay
-        :rtype: bool
-        """
-
-        return self._autoplay
-
-    @autoplay.setter
-    def autoplay(self, value: bool) -> None:
-        if not isinstance(value, bool):
-            return TypeError("`autoplay` property must be `bool`.")
-
-        self._autoplay = value
-
-    @property
-    def filter(self) -> dict:
-        """
-        Current filter value of the voice client
-
-        :getter: Returns VoiceClient filter
-        :setter: Set VoiceClient filter
-        :rtype: dict
-        """
-
-        return self._filter
-
-    @filter.setter
-    def filter(self, value: dict) -> None:
-        if not isinstance(value, dict):
-            return TypeError("`filter` property must be `dict`.")
-
-        self._filter = value
-
-    @property
-    def current(self) -> Union[AudioSource, AudioData]:
-        """
-        Current source of the voice client
-
-        :getter: Returns current source
-        :rtype: :class:`AudioSource` or :class:`AudioData`
-        """
+    def current(self):
+        if not self.player:
+            return
 
         return self.player.current
 
     @property
-    def next(self) -> Union[AudioSource, AudioData]:
-        """
-        Next source of the voice client
-
-        :getter: Returns next source
-        :rtype: :class:`AudioSource` or :class:`AudioData`
-        """
+    def next(self):
+        if not self.player:
+            return
 
         return self.player.next
 
-    def putSource(self, Source: Union[list, AudioData, AudioSource]) -> int:
-        """
-        Put source to Queue.
+    async def getSource(self, query):
+        return await AudioData.create(query)
 
-        :param Source: list or :class:`AudioSource` or :class:`AudioData` to put on Queue
-        :rtype: int
-        """
+    def putSource(self, source):
+        sources = source if isinstance(source, list) else [source]
 
-        if not isinstance(Source, (list, AudioData, AudioSource)):
-            raise TypeError("`Source` must be `list` or `AudioData` or `AudioSource`.")
+        self.Queue.extend(sources)
 
-        self.Queue.extend(Source if isinstance(Source, list) else [Source])
-
-        self.dispatcher.dispatch(
-            "putSource", sources=(Source if isinstance(Source, list) else [Source])
-        )
+        self.dispatcher.dispatch("putSource", sources=sources)
 
         return (
-            self.Queue.index(Source)
-            if not isinstance(Source, list)
-            else [self.Queue.index(Item) for Item in Source]
+            self.Queue.index(source)
+            if not isinstance(source, list)
+            else list(map(self.Queue.index, sources))
         )
 
-    async def getSource(self, Query: str) -> AudioData:
-        """
-        Get source from Query
+    async def loadSource(self, query, **kwargs):
+        source = await self.getSource(query)
 
-        :param str Query: Query to search
-        :rtype: :class:`AudioData`
-        """
+        if isinstance(kwargs.get("related"), bool):
+            source.related = kwargs["related"]
 
-        return await AudioData.create(Query)
+        self.putSource(source)
 
-    async def loadSource(self, Query: str, **kwargs) -> AudioData:
-        """
-        Get source from Query and put it on Queue.
+        self.dispatcher.dispatch("loadSource", source=source, **kwargs)
 
-        :param str Query: Query to search
-        :rtype: :class:`AudioData`
-        """
-
-        Data = await self.getSource(Query)
-
-        if "related" in kwargs and isinstance(kwargs["related"], bool):
-            Data.related = kwargs["related"]
-
-        Index = self.putSource(Data)
-
-        self.dispatcher.dispatch(
-            "loadSource",
-            source=(
-                {"data": Data, "index": Index}
-                if not isinstance(Data, list)
-                else list(
-                    map(
-                        lambda zipped: {"data": zipped[0], "index": zipped[1]},
-                        zip(Data, Index),
-                    )
-                )
-            ),
-            **kwargs,
-        )
-
-        return Data
-
-    async def seek(self, offset: int) -> None:
-        """
-        Seek current playing source.
-
-        :param float offset: offset to seek
-        :rtype: None
-        """
+        return source
 
+    async def seek(self, offset):
         return await self.player.seek(offset)
 
-    def skip(self, offset: int = 1) -> None:
-        """
-        skip current playing source.
-
-        :param float offset: offset to skip
-        :rtype: None
-        """
-
+    def skip(self, offset=1):
         if not self.player.current:
             raise NotPlaying
 
@@ -324,36 +186,18 @@ def skip(self, offset: int = 1) -> None:
 
         self.player.current.skip()
 
-    def pause(self) -> bool:
-        """
-        Pause playing
-
-        :rtype: True
-        """
-
+    def pause(self):
         self.paused = True
-        return True
-
-    def resume(self) -> bool:
-        """
-        Resume playing
 
-        :rtype: True
-        """
+        return self.paused
 
+    def resume(self):
         self.paused = False
-        return False
 
-    def shuffle(self) -> dict:
-        """
-        Shuffle Queue.
-
-        :rtype: dict
-        """
+        return self.paused
 
+    def shuffle(self):
         if not self.Queue:
             raise ValueError("`Queue` is empty now.")
 
         random.shuffle(self.Queue)
-
-        return self.Queue
diff --git a/docs/.tx/config b/docs/.tx/config
new file mode 100644
index 00000000..47a4a3e1
--- /dev/null
+++ b/docs/.tx/config
@@ -0,0 +1,62 @@
+[main]
+host = https://www.transifex.com
+
+[discodo.docs__build_gettext_index]
+file_filter = locale/<lang>/LC_MESSAGES/index.po
+source_file = _build/gettext/index.pot
+source_lang = en
+type = PO
+
+[discodo.docs__build_gettext_introduction_installation]
+file_filter = locale/<lang>/LC_MESSAGES/introduction/installation.po
+source_file = _build/gettext/introduction/installation.pot
+source_lang = en
+type = PO
+
+[discodo.docs__build_gettext_introduction_logging]
+file_filter = locale/<lang>/LC_MESSAGES/introduction/logging.po
+source_file = _build/gettext/introduction/logging.pot
+source_lang = en
+type = PO
+
+[discodo.docs__build_gettext_introduction_quickstart]
+file_filter = locale/<lang>/LC_MESSAGES/introduction/quickstart.po
+source_file = _build/gettext/introduction/quickstart.pot
+source_lang = en
+type = PO
+
+[discodo.docs__build_gettext_node_connection]
+file_filter = locale/<lang>/LC_MESSAGES/node/connection.po
+source_file = _build/gettext/node/connection.pot
+source_lang = en
+type = PO
+
+[discodo.docs__build_gettext_node_resources_voice_client]
+file_filter = locale/<lang>/LC_MESSAGES/node/resources/voice_client.po
+source_file = _build/gettext/node/resources/voice_client.pot
+source_lang = en
+type = PO
+
+[discodo.docs__build_gettext_node_resources_source]
+file_filter = locale/<lang>/LC_MESSAGES/node/resources/source.po
+source_file = _build/gettext/node/resources/source.pot
+source_lang = en
+type = PO
+
+[discodo.docs__build_gettext_node_resources_events]
+file_filter = locale/<lang>/LC_MESSAGES/node/resources/events.po
+source_file = _build/gettext/node/resources/events.pot
+source_lang = en
+type = PO
+
+[discodo.docs__build_gettext_high_api]
+file_filter = locale/<lang>/LC_MESSAGES/high_api.po
+source_file = _build/gettext/high_api.pot
+source_lang = en
+type = PO
+
+[discodo.docs__build_gettext_events]
+file_filter = locale/<lang>/LC_MESSAGES/events.po
+source_file = _build/gettext/events.pot
+source_lang = en
+type = PO
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 00000000..d4bb2cbb
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/README.md b/docs/README.md
deleted file mode 100644
index 92818e00..00000000
--- a/docs/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Welcome to Discodo Documentation!
-
-Discodo is an enhanced audio player for discord.
-
-## Features
-- Standalone Audio Node
-- Youtube Related Video Autoplay
-- Crossfade and Audio effects
-- Synced Youtube Video Subtitle
-
-## Supported sources
-
-+ All sources that can be extracted from [youtube_dl](https://github.com/ytdl-org/youtube-dl)
-+ All formats that can be demuxed by [libav](https://libav.org/)
-
-## Client libraries
-
-* [discodo](https://github.com/kijk2869/discodo) (Python, discord.py)
-* ~~[discodo.js](Under Developing) (Node.js, discord.js)~~
-
-## Content
-
-- [Client](./client)
-- [Server](./server)
diff --git a/docs/client/README.md b/docs/client/README.md
deleted file mode 100644
index e4e5a37e..00000000
--- a/docs/client/README.md
+++ /dev/null
@@ -1,1 +0,0 @@
-# Working on...
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 00000000..5c2faad3
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,75 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath(".."))
+
+
+# -- Project information -----------------------------------------------------
+
+project = "discodo"
+copyright = "2021, kijk2869"
+author = "kijk2869"
+
+# The full version, including alpha/beta/rc tags
+from discodo import __version__
+
+release = f"v{__version__}"
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    "sphinx.ext.autodoc",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.todo",
+    "sphinx.ext.viewcode",
+    "sphinx_rtd_theme",
+    "sphinxcontrib.httpdomain",
+]
+
+intersphinx_mapping = {
+    "python": ("https://docs.python.org/3", None),
+    "discord": ("https://discordpy.readthedocs.io/en/latest/", None),
+}
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = "sphinx_rtd_theme"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+# -- Options for sphinx-intl example
+
+language = None
+locale_dirs = ["locale/"]
+gettext_compact = False
diff --git a/docs/events.rst b/docs/events.rst
new file mode 100644
index 00000000..2bd23a93
--- /dev/null
+++ b/docs/events.rst
@@ -0,0 +1,216 @@
+.. _client_events:
+
+Event Reference
+===============
+
+This section outlines the different types of events dispatched by discodo client.
+
+.. note::
+    If you are using a standalone discodo node server while not using :py:class:`DPYClient`, the events that you get will have something different. See this :ref:`node_events`.
+
+To listen an event, use :py:class:`EventDispatcher` of the :py:class:`DPYClient`
+
+.. code-block:: python
+
+    import discord
+    import discodo
+
+    bot = discord.Client()
+    codo = discodo.DPYClient(bot)
+
+    # Using DPYClient.event
+
+    @codo.event("SOURCE_START")
+    async def start_event(VC, source):
+        print(f"{VC} is now playing {source}")
+
+    # Using DPYClient.dispatcher.on
+
+    async def stop_event(VC, source):
+        print(f"{VC} is stopped {source}")
+
+    codo.dispatcher.on("SOURCE_STOP", stop_event)
+
+VC_CREATED(:py:class:`VoiceClient`, :py:class:`dict` data)
+----------------------------------------------------------
+
+Called when the new voice client has successfully created. This is not the same as the client being fully connected.
+
+.. _VC_CREATED_DATA_STRUCTURE:
+
+Data Structure
+~~~~~~~~~~~~~~
+
+================ ===================== ==================================
+ Field            Type                  Description
+---------------- --------------------- ----------------------------------
+ guild_id         int                   The guild id of the voice client
+---------------- --------------------- ----------------------------------
+ id               str                   The id of the voice client
+================ ===================== ==================================
+
+QUEUE_EVENT(:py:class:`VoiceClient`, :py:class:`dict` data)
+-----------------------------------------------------------
+
+Called when there is something changed in the queue of the voice client. If you are using :py:class:`DPYClient`, Ignore this event.
+
+.. _QUEUE_EVENT_DATA_STRUCTURE:
+
+Data Structure
+~~~~~~~~~~~~~~
+
+================ ===================== ==================================
+ Field            Type                  Description
+---------------- --------------------- ----------------------------------
+ guild_id         int                   The guild id of the voice client
+---------------- --------------------- ----------------------------------
+ name             str                   The name of the method
+---------------- --------------------- ----------------------------------
+ args             list                  The arguments of the method
+================ ===================== ==================================
+
+VC_CHANNEL_EDITED(:py:class:`VoiceClient`, :py:class:`dict` data)
+-----------------------------------------------------------------
+
+Called when the voice channel of the voice client is changed.
+
+.. _VC_CHANNEL_EDITED_DATA_STRUCTURE:
+
+Data Structure
+~~~~~~~~~~~~~~
+
+================ ===================== ===================================
+ Field            Type                  Description
+---------------- --------------------- -----------------------------------
+ guild_id         int                   The guild id of the voice client
+---------------- --------------------- -----------------------------------
+ channel_id       int                   The channel id of the voice client
+================ ===================== ===================================
+
+putSource(:py:class:`VoiceClient`, :py:class:`dict` data)
+---------------------------------------------------------
+
+Called when some sources are put in the queue.
+
+.. _putSource_DATA_STRUCTURE:
+
+Data Structure
+~~~~~~~~~~~~~~
+
+================ ===================== ===================================
+ Field            Type                  Description
+---------------- --------------------- -----------------------------------
+ guild_id         int                   The guild id of the voice client
+---------------- --------------------- -----------------------------------
+ sources          list                  The sources which is put
+================ ===================== ===================================
+
+loadSource(:py:class:`VoiceClient`, :py:class:`dict` data)
+----------------------------------------------------------
+
+Called when some sources are searched and put in the queue.
+
+.. _loadSource_DATA_STRUCTURE:
+
+Data Structure
+~~~~~~~~~~~~~~
+
+================ ======================= =======================================
+ Field            Type                    Description
+---------------- ----------------------- ---------------------------------------
+ guild_id         int                     The guild id of the voice client
+---------------- ----------------------- ---------------------------------------
+ source           Union[AudioData, list]  The sources which is searched and put
+================ ======================= =======================================
+
+REQUIRE_NEXT_SOURCE(:py:class:`VoiceClient`, :py:class:`dict` data)
+-------------------------------------------------------------------
+
+Called when the player needs next source to play. If you set ``autoplay`` as ``True``, the related source will be put after this event.
+
+.. _REQUIRE_NEXT_SOURCE_DATA_STRUCTURE:
+
+Data Structure
+~~~~~~~~~~~~~~
+
+================ ======================= ==================================================
+ Field            Type                    Description
+---------------- ----------------------- --------------------------------------------------
+ guild_id         int                     The guild id of the voice client
+---------------- ----------------------- --------------------------------------------------
+ current          AudioSource             The source which the player is currently playing
+================ ======================= ==================================================
+
+SOURCE_START(:py:class:`VoiceClient`, :py:class:`dict` data)
+------------------------------------------------------------
+
+Called when the player starts to play the source.
+
+.. _SOURCE_START_DATA_STRUCTURE:
+
+Data Structure
+~~~~~~~~~~~~~~
+
+================ ======================= ============================================
+ Field            Type                    Description
+---------------- ----------------------- --------------------------------------------
+ guild_id         int                     The guild id of the voice client
+---------------- ----------------------- --------------------------------------------
+ source           AudioSource             The source which the player starts to play
+================ ======================= ============================================
+
+SOURCE_STOP(:py:class:`VoiceClient`, :py:class:`dict` data)
+-----------------------------------------------------------
+
+Called when the player stops to play the source.
+
+.. _SOURCE_STOP_DATA_STRUCTURE:
+
+Data Structure
+~~~~~~~~~~~~~~
+
+================ ======================= ============================================
+ Field            Type                    Description
+---------------- ----------------------- --------------------------------------------
+ guild_id         int                     The guild id of the voice client
+---------------- ----------------------- --------------------------------------------
+ source           AudioSource             The source which the player stops to play
+================ ======================= ============================================
+
+PLAYER_TRACEBACK(:py:class:`VoiceClient`, :py:class:`dict` data)
+----------------------------------------------------------------
+
+Called when the player gets some traceback while trying to send packets to discord server.
+
+.. _PLAYER_TRACEBACK_DATA_STRUCTURE:
+
+Data Structure
+~~~~~~~~~~~~~~
+
+================ =============================== ================================================================
+ Field            Type                            Description
+---------------- ------------------------------- ----------------------------------------------------------------
+ guild_id         int                             The guild id of the voice client
+---------------- ------------------------------- ----------------------------------------------------------------
+ traceback        str                             The traceback information which the player gets
+================ =============================== ================================================================
+
+SOURCE_TRACEBACK(:py:class:`VoiceClient`, :py:class:`dict` data)
+----------------------------------------------------------------
+
+Called when the player gets some traceback while trying to play the source. That source will be automatically removed from the queue.
+
+.. _SOURCE_TRACEBACK_DATA_STRUCTURE:
+
+Data Structure
+~~~~~~~~~~~~~~
+
+================ =============================== ================================================================
+ Field            Type                            Description
+---------------- ------------------------------- ----------------------------------------------------------------
+ guild_id         int                             The guild id of the voice client
+---------------- ------------------------------- ----------------------------------------------------------------
+ source           Union[AudioData, AudioSource]   The source which the player gets traceback while trying to play
+---------------- ------------------------------- ----------------------------------------------------------------
+ traceback        str                             The traceback information which the player gets
+================ =============================== ================================================================
diff --git a/docs/high_api.rst b/docs/high_api.rst
new file mode 100644
index 00000000..b007aa4d
--- /dev/null
+++ b/docs/high_api.rst
@@ -0,0 +1,87 @@
+.. currentmodule:: discodo
+
+High Level API
+==============
+
+This section outlines the API of discodo.
+
+.. note::
+
+    This module uses the Python logging module to log diagnostic and errors
+    in an output independent way. If the logging module is not configured,
+    logs will not be output anywhere. See :ref:`logging_setup` for
+    more information.
+
+Client
+------
+
+.. autoclass:: discodo.DPYClient
+    :members:
+
+Utils
+-----
+
+.. autoclass:: discodo.utils.EventDispatcher
+    :members:
+
+.. automodule:: discodo.utils.status
+    :members:
+
+.. autoclass:: discodo.utils.CallbackList
+    :members:
+
+.. automethod:: discodo.utils.tcp.getFreePort
+
+Node
+----
+
+.. autoclass:: discodo.Node
+    :members:
+
+.. autoclass:: discodo.Nodes
+    :members:
+
+VoiceClient
+-----------
+
+.. autoclass:: discodo.VoiceClient
+    :members:
+
+AudioData
+---------
+
+.. autoclass:: discodo.models.AudioData
+    :members:
+
+AudioSource
+-----------
+
+.. autoclass:: discodo.models.AudioSource
+    :members:
+
+Errors
+------
+
+.. autoexception:: discodo.DiscodoException
+
+.. autoexception:: discodo.EncryptModeNotReceived
+
+.. autoexception:: discodo.NotPlaying
+
+.. autoexception:: discodo.VoiceClientNotFound
+
+.. autoexception:: discodo.NoSearchResults
+
+.. autoexception:: discodo.OpusLoadError
+
+.. autoexception:: discodo.HTTPException
+
+.. autoexception:: discodo.Forbidden
+
+.. autoexception:: discodo.TooManyRequests
+
+.. autoexception:: discodo.NotSeekable
+
+.. autoexception:: discodo.NodeException
+
+.. autoexception:: discodo.NodeNotConnected
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 00000000..9443912a
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,62 @@
+.. discodo documentation master file, created by
+   sphinx-quickstart on Sun Feb 28 13:00:50 2021.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to discodo's documentation!
+===================================
+
+Discodo is an enhanced audio player for discord.
+
+Features
+--------
+
+- Standalone Audio Node
+- Youtube Related Video Autoplay
+- Crossfade and Audio effects
+- Synced Youtube Video Subtitle
+
+Supported sources
+-----------------
+
+- All sources that can be extracted from youtube-dl_
+- All formats that can be demuxed by libav_
+
+.. _youtube-dl: https://github.com/ytdl-org/youtube-dl
+.. _libav: https://libav.org/
+
+Client libraries
+----------------
+
+- discodo_ (Python_)
+- discodo.js_ (Under Developing) (Node.js_)
+
+.. _Python: https://www.python.org/
+.. _Node.js: https://nodejs.org/
+.. _discodo: https://github.com/kijk2869/discodo
+.. _discodo.js: Under Developing
+
+
+.. toctree::
+   :caption: Introduction
+   :maxdepth: 1
+
+   introduction/installation.rst
+   introduction/quickstart.rst
+   introduction/logging.rst
+
+.. toctree::
+   :caption: Node
+   :maxdepth: 1
+
+   node/connection.rst
+   node/resources/voice_client.rst
+   node/resources/source.rst
+   node/resources/events.rst
+
+.. toctree::
+   :caption: API Reference
+   :maxdepth: 1
+
+   high_api.rst
+   events.rst
diff --git a/docs/introduction/installation.rst b/docs/introduction/installation.rst
new file mode 100644
index 00000000..c66be548
--- /dev/null
+++ b/docs/introduction/installation.rst
@@ -0,0 +1,159 @@
+Installation
+============
+
+Prerequisites
+-------------
+
+Discodo works with **Python 3.7 or higher.**
+
+Ealier versions of Python may not be worked.
+
+Dependencies
+------------
+
+On Linux environments, below dependencies are required:
+
+- ``python3-dev``
+- ``libopus-dev``
+- ``libnacl-dev``
+
+PyAV depends upon several libraries from FFmpeg:
+
+- ``libavcodec-dev``
+- ``libavdevice-dev``
+- ``libavfilter-dev``
+- ``libavformat-dev``
+- ``libavutil-dev``
+- ``libswresample-dev``
+- ``libswscale-dev``
+- ``pkg-config``
+
+Mac OS X
+^^^^^^^^
+
+.. code-block:: console
+
+    $ brew install opus pkg-config ffmpeg
+
+Ubuntu
+^^^^^^
+
+.. code-block:: console
+
+    $ sudo apt update
+
+    # Our general dependencies
+    $ sudo apt install --no-install-recommends -y python3-dev libopus-dev libnacl-dev
+
+    # PyAV dependencies
+    $ sudo apt install --no-install-recommends -y \
+        pkg-config libavformat-dev libavcodec-dev libavdevice-dev \
+        libavutil-dev libswscale-dev libavresample-dev libavfilter-dev
+
+Installing
+----------
+
+PyPI
+^^^^
+
+.. code-block:: console
+
+    $ python3 -m pip install -U discodo
+
+Docker
+^^^^^^
+
+.. code-block:: console
+
+    $ docker pull kijk2869/discodo:release-2.3.13
+
+Execution
+---------
+
+You can see additional options with the ``--help`` flag.
+
+.. code-block:: console
+
+    $ python3 -m discodo [-h] [--version] [--config CONFIG] [--config-json CONFIG_JSON] [--host HOST] [--port PORT]
+               [--auth AUTH] [--ws-interval WS_INTERVAL] [--ws-timeout WS_TIMEOUT] [--ip IP] [--exclude-ip EXCLUDE_IP]
+               [--default-volume DEFAULT_VOLUME] [--default-crossfade DEFAULT_CROSSFADE]
+               [--default-autoplay DEFAULT_AUTOPLAY] [--bufferlimit BUFFERLIMIT] [--preload PRELOAD]
+               [--timeout TIMEOUT] [--enabled-resolver ENABLED_RESOLVER] [--spotify-id SPOTIFY_ID]
+               [--spotify-secret SPOTIFY_SECRET] [--verbose]
+
+Options
+-------
+
+.. code-block:: console
+
+    optional arguments:
+    -h, --help            show this help message and exit
+    --version             Config json file path (default: None)
+    --config CONFIG       Config json file path (default: None)
+    --config-json CONFIG_JSON
+                            Config json string (default: None)
+
+    Webserver Option:
+    --host HOST, -H HOST  the hostname to listen on (default: 0.0.0.0)
+    --port PORT, -P PORT  the port of the webserver (default: 8000)
+    --auth AUTH, -A AUTH  the password of the webserver (default: hellodiscodo)
+    --ws-interval WS_INTERVAL
+                            heartbeat interval between discodo server and client (default: 15)
+    --ws-timeout WS_TIMEOUT
+                            seconds to close connection there is no respond from client (default: 60)
+
+    Network Option:
+    --ip IP               Client-side IP blocks to use
+    --exclude-ip EXCLUDE_IP
+                            Client-side IP addresses not to use
+
+    Player Option:
+    --default-volume DEFAULT_VOLUME
+                            player's default volume (default: 100)
+    --default-crossfade DEFAULT_CROSSFADE
+                            player's default crossfade seconds (default: 10.0)
+    --default-autoplay DEFAULT_AUTOPLAY
+                            player's default auto related play state (default: True)
+    --bufferlimit BUFFERLIMIT
+                            seconds of audio will be loaded in buffer (default: 5)
+    --preload PRELOAD     seconds to load next song before this song ends (default: 10)
+    --timeout TIMEOUT     seconds to cleanup player when connection of discord terminated (default: 300)
+
+    Extra Extractor Option:
+    --enabled-resolver ENABLED_RESOLVER
+                            Extra resolvers to enable (Support melon and spotify)
+    --spotify-id SPOTIFY_ID
+                            Spotify API id (default: None)
+    --spotify-secret SPOTIFY_SECRET
+                            Spotify API secret (default: None)
+
+    Logging Option:
+    --verbose, -v         Print various debugging information
+
+Config file
+^^^^^^^^^^^
+
+.. code-block:: json
+
+    {
+        "HOST": "0.0.0.0",
+        "PORT": 8000,
+        "PASSWORD": "hellodiscodo",
+        "HANDSHAKE_INTERVAL": 15,
+        "HANDSHAKE_TIMEOUT": 60,
+        "IPBLOCKS": [],
+        "EXCLUDEIPS": [],
+        "DEFAULT_AUTOPLAY": true,
+        "DEFAULT_VOLUME": 1,
+        "DEFAULT_CROSSFADE": 10,
+        "DEFAULT_GAPLESS": false,
+        "BUFFERLIMIT": 5,
+        "PRELOAD_TIME": 10,
+        "VCTIMEOUT": 300,
+        "ENABLED_EXT_RESOLVER": [
+            "melon",
+            "vibe"
+        ],
+        "SPOTIFY_ID": null,
+        "SPOTIFY_SECRET": null
+    }
diff --git a/docs/introduction/logging.rst b/docs/introduction/logging.rst
new file mode 100644
index 00000000..06801200
--- /dev/null
+++ b/docs/introduction/logging.rst
@@ -0,0 +1,45 @@
+.. _logging_setup:
+
+Setting Up Logging
+==================
+
+Basic logging
+-------------
+
+discodo_ logs several information via the logging_ python module like discord.py_. It is strongly recommended to configure the logging module, as you can't see some error if it is not set up. You can configure the logging_ module as simple as:
+
+.. _discodo: https://github.com/kijk2869/discodo
+.. _logging: https://docs.python.org/3/library/logging.html#module-logging
+.. _discord.py: https://github.com/Rapptz/discord.py
+.. _rich: https://github.com/willmcgugan/rich
+
+.. code-block:: python3
+
+    import logging
+
+    logging.basicConfig(level=logging.INFO)
+
+Placed at the start of the code. This will output the logs from all libraries which use the logging_ module, including discodo_, to the console.
+
+The ``level`` argument specifies what level of events to log and can be any of ``CRITICAL``, ``ERROR``, ``WARNING``, ``INFO``, and ``DEBUG`` and default value is ``WARNING``
+
+For more information, check the documentation of the logging_ module.
+
+Use rich formatter
+-------------------
+
+Also, discodo_ uses rich_ to improve log readability. rich_ is an library for rich text and formatting in the terminal. To output the log to the terminal using rich_, you can set it as follows:
+
+.. code-block:: python3
+
+    import logging
+    from rich.logging import RichHandler
+
+    logging.basicConfig(level=logging.INFO, format="%(name)s :\t%(message)s", handlers=[RichHandler(rich_tracebacks=True)])
+
+Because rich_ displays the ``log level`` separately, remove the ``level`` from the ``format`` argument, and set ``rich_tracebacks`` to ``True`` for formatting tracebacks.
+
+When you set this, the log will be formatted as follows:
+
+.. image:: rich.png
+    :alt: Rich formatter example
diff --git a/docs/introduction/quickstart.rst b/docs/introduction/quickstart.rst
new file mode 100644
index 00000000..0da752d3
--- /dev/null
+++ b/docs/introduction/quickstart.rst
@@ -0,0 +1,90 @@
+Quickstart
+==========
+
+A Minimal Bot with discord.py_
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. _discord.py: https://github.com/Rapptz/discord.py
+
+Let’s make a bot which uses local node feature.
+
+It looks like this:
+
+.. code-block:: python3
+
+    import discord
+    import discodo
+
+    client = discord.Client()
+    codo = discodo.DPYClient(client)
+
+    @client.event
+    async def on_ready():
+        print(f"I logged in as {client.user} (client.user.id)")
+
+    @codo.event("SOURCE_START")
+    async def sendPlaying(VC, Data):
+        await VC.channel.send(f"I'm now playing {Data['source']['title']}")
+
+    @client.event
+    async def on_message(message):
+        if message.author == client.user:
+            return
+
+        if message.content.startswith("!join"):
+            if not message.author.voice:
+                return await message.channel.send("Join the voice channel first.")
+
+            await codo.connect(message.author.voice.channel)
+
+            return await message.channel.send(f"I connected to {message.author.voice.channel.mention}")
+
+        if message.content.startswith("!play"):
+            VC = codo.getVC(message.guild, safe=True)
+
+            if not VC:
+                return await message.channel.send("Please type `!join` first.")
+
+            if not hasattr(VC, "channel"):
+                VC.channel = message.channel
+
+            source = await VC.loadSource(message.content[5:].strip())
+
+            if isinstance(source, list):
+                return await message.channel.send(f"{len(source) - 1} songs except {source[0].title} added.")
+            else:
+                return await message.channel.send(f"{source.title} added.")
+
+        if message.content.startswith("!stop"):
+            VC = codo.getVC(message.guild, safe=True)
+
+            if not VC:
+                return await message.channel.send("I'm not connected to any voice channel now.")
+
+            await VC.destroy()
+
+            return await message.channel.send("I stopped the player and cleaned the queue.")
+
+    codo.registerNode()
+    client.run("your discord bot token here")
+
+
+Let's name this file ``simple_musicbot.py``
+
+Assume you know how to use discord.py_, and I will explain the discodo_ code step by step.
+
+.. _discodo: https://github.com/kijk2869/discodo
+
+1. We create an instance of :py:class:`DPYClient`. This client will manage voice connections to Discord.
+2. After ``on_ready`` event, we use the :py:attr:`DPYClient.event()` decorator to register an event like discord.py_ . In this case, ``SOURCE_START`` will be called when the music starts playing.
+3. When the ``!join`` command is excuted, we check if the :py:attr:`discord.Message.author` is connected to the voice channel. If it is, then we connected to the channel using :py:func:`DPYClient.connect()`
+4. When the ``!play`` command runs, set the VC.channel to the current message channel to send messages during playback, search for queries and add them to the list.
+5. If the ``!stop`` command is excuted, we destroy the voice client via :py:func:`VoiceClient.destroy()`
+6. Finally, we set local nodes to be used by not giving host argument to :py:func:`DPYClient.registerNode()`
+
+Now that we've made a simple music bot, we have to run this. Just as you do when you run a discord.py_ Bot
+
+.. code-block:: console
+
+    $ python simple_musicbot.py
+
+Now you can try playing around with your basic musicbot.
diff --git a/docs/introduction/rich.png b/docs/introduction/rich.png
new file mode 100644
index 00000000..ca1b6d39
Binary files /dev/null and b/docs/introduction/rich.png differ
diff --git a/docs/locale/ko/LC_MESSAGES/events.po b/docs/locale/ko/LC_MESSAGES/events.po
new file mode 100644
index 00000000..06d04ea7
--- /dev/null
+++ b/docs/locale/ko/LC_MESSAGES/events.po
@@ -0,0 +1,288 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2021, kijk2869
+# This file is distributed under the same license as the discodo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# Translators:
+# Mary <mareowst@gmail.com>, 2021
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: discodo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-03-07 18:34+0900\n"
+"PO-Revision-Date: 2021-03-07 09:35+0000\n"
+"Last-Translator: Mary <mareowst@gmail.com>, 2021\n"
+"Language-Team: Korean (https://www.transifex.com/mary-2/teams/117665/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../events.rst:4
+msgid "Event Reference"
+msgstr "이벤트 레퍼런스"
+
+#: ../../events.rst:6
+msgid ""
+"This section outlines the different types of events dispatched by discodo "
+"client."
+msgstr "이 부분에서는 discodo 클라이언트에서 발송한 여러 유형의 이벤트에 대해 설명합니다."
+
+#: ../../events.rst:9
+msgid ""
+"If you are using a standalone discodo node server while not using "
+":py:class:`DPYClient`, the events that you get will have something "
+"different. See this :ref:`node_events`."
+msgstr ""
+":py:class:`DPYClient` 를 사용하지 않는 동안 독립 실행형 discodo 노드 서버를 사용하는 경우 가져오는 이벤트가 "
+"다를 수 있습니다. 대신 :ref:`node_events` 를 참조하십시오."
+
+#: ../../events.rst:11
+msgid ""
+"To listen an event, use :py:class:`EventDispatcher` of the "
+":py:class:`DPYClient`"
+msgstr "이벤트를 수신하려면 :py:class:`EventDispatcher` 를 사용하십시오."
+
+#: ../../events.rst:35
+msgid "VC_CREATED(:py:class:`VoiceClient`, :py:class:`dict` data)"
+msgstr "VC_CREATED(:py:class:`VoiceClient`, :py:class:`dict` data)"
+
+#: ../../events.rst:37
+msgid ""
+"Called when the new voice client has successfully created. This is not the "
+"same as the client being fully connected."
+msgstr "새로운 음성 클라이언트가 성공적으로 생성되었을 때 호출됩니다. 이것은 클라이언트가 완전히 연결되었다는 의미는 아닙니다."
+
+#: ../../events.rst:42 ../../events.rst:60 ../../events.rst:80
+#: ../../events.rst:98 ../../events.rst:116 ../../events.rst:134
+#: ../../events.rst:152 ../../events.rst:170 ../../events.rst:188
+#: ../../events.rst:206
+msgid "Data Structure"
+msgstr "데이터 구조"
+
+#: ../../events.rst:45 ../../events.rst:63 ../../events.rst:83
+#: ../../events.rst:101 ../../events.rst:119 ../../events.rst:137
+#: ../../events.rst:155 ../../events.rst:173 ../../events.rst:191
+#: ../../events.rst:209
+msgid "Field"
+msgstr "이름"
+
+#: ../../events.rst:45 ../../events.rst:63 ../../events.rst:83
+#: ../../events.rst:101 ../../events.rst:119 ../../events.rst:137
+#: ../../events.rst:155 ../../events.rst:173 ../../events.rst:191
+#: ../../events.rst:209
+msgid "Type"
+msgstr "자료형"
+
+#: ../../events.rst:45 ../../events.rst:63 ../../events.rst:83
+#: ../../events.rst:101 ../../events.rst:119 ../../events.rst:137
+#: ../../events.rst:155 ../../events.rst:173 ../../events.rst:191
+#: ../../events.rst:209
+msgid "Description"
+msgstr "설명"
+
+#: ../../events.rst:47 ../../events.rst:65 ../../events.rst:85
+#: ../../events.rst:103 ../../events.rst:121 ../../events.rst:139
+#: ../../events.rst:157 ../../events.rst:175 ../../events.rst:193
+#: ../../events.rst:211
+msgid "guild_id"
+msgstr "guild_id"
+
+#: ../../events.rst:47 ../../events.rst:65 ../../events.rst:85
+#: ../../events.rst:87 ../../events.rst:103 ../../events.rst:121
+#: ../../events.rst:139 ../../events.rst:157 ../../events.rst:175
+#: ../../events.rst:193 ../../events.rst:211
+msgid "int"
+msgstr "정수"
+
+#: ../../events.rst:47 ../../events.rst:65 ../../events.rst:85
+#: ../../events.rst:103 ../../events.rst:121 ../../events.rst:139
+#: ../../events.rst:157 ../../events.rst:175 ../../events.rst:193
+#: ../../events.rst:211
+msgid "The guild id of the voice client"
+msgstr "음성 클라이언트의 길드 아이디"
+
+#: ../../events.rst:49
+msgid "id"
+msgstr "id"
+
+#: ../../events.rst:49 ../../events.rst:67 ../../events.rst:195
+#: ../../events.rst:215
+msgid "str"
+msgstr "문자열"
+
+#: ../../events.rst:49
+msgid "The id of the voice client"
+msgstr "음성 클라이언트의 아이디"
+
+#: ../../events.rst:53
+msgid "QUEUE_EVENT(:py:class:`VoiceClient`, :py:class:`dict` data)"
+msgstr "QUEUE_EVENT(:py:class:`VoiceClient`, :py:class:`dict` data)"
+
+#: ../../events.rst:55
+msgid ""
+"Called when there is something changed in the queue of the voice client. If "
+"you are using :py:class:`DPYClient`, Ignore this event."
+msgstr ""
+"음성 클라이언트의 대기열에 변경된 내용이 있으면 호출됩니다. :py:class:`DPYClient` 를 사용하는 경우 이 이벤트를 "
+"무시하십시오."
+
+#: ../../events.rst:67
+msgid "name"
+msgstr "name"
+
+#: ../../events.rst:67
+msgid "The name of the method"
+msgstr "함수의 이름"
+
+#: ../../events.rst:69
+msgid "args"
+msgstr "args"
+
+#: ../../events.rst:69 ../../events.rst:105
+msgid "list"
+msgstr "리스트"
+
+#: ../../events.rst:69
+msgid "The arguments of the method"
+msgstr "함수의 인자들"
+
+#: ../../events.rst:73
+msgid "VC_CHANNEL_EDITED(:py:class:`VoiceClient`, :py:class:`dict` data)"
+msgstr "VC_CHANNEL_EDITED(:py:class:`VoiceClient`, :py:class:`dict` data)"
+
+#: ../../events.rst:75
+msgid "Called when the voice channel of the voice client is changed."
+msgstr "음성 클라이언트의 음성 채널이 변경되면 호출됩니다."
+
+#: ../../events.rst:87
+msgid "channel_id"
+msgstr "channel_id"
+
+#: ../../events.rst:87
+msgid "The channel id of the voice client"
+msgstr "음성 클라이언트의 채널 아이디"
+
+#: ../../events.rst:91
+msgid "putSource(:py:class:`VoiceClient`, :py:class:`dict` data)"
+msgstr "putSource(:py:class:`VoiceClient`, :py:class:`dict` data)"
+
+#: ../../events.rst:93
+msgid "Called when some sources are put in the queue."
+msgstr "일부 소스를 대기열에 넣으면 호출됩니다."
+
+#: ../../events.rst:105
+msgid "sources"
+msgstr "sources"
+
+#: ../../events.rst:105
+msgid "The sources which is put"
+msgstr "추가된 소스"
+
+#: ../../events.rst:109
+msgid "loadSource(:py:class:`VoiceClient`, :py:class:`dict` data)"
+msgstr "loadSource(:py:class:`VoiceClient`, :py:class:`dict` data)"
+
+#: ../../events.rst:111
+msgid "Called when some sources are searched and put in the queue."
+msgstr "일부 소스를 검색하여 대기열에 넣으면 호출됩니다."
+
+#: ../../events.rst:123 ../../events.rst:159 ../../events.rst:177
+#: ../../events.rst:213
+msgid "source"
+msgstr "source"
+
+#: ../../events.rst:123
+msgid "Union[AudioData, list]"
+msgstr "Union[AudioData, 리스트]"
+
+#: ../../events.rst:123
+msgid "The sources which is searched and put"
+msgstr "검색되고 추가되는 소스"
+
+#: ../../events.rst:127
+msgid "REQUIRE_NEXT_SOURCE(:py:class:`VoiceClient`, :py:class:`dict` data)"
+msgstr "REQUIRE_NEXT_SOURCE(:py:class:`VoiceClient`, :py:class:`dict` data)"
+
+#: ../../events.rst:129
+msgid ""
+"Called when the player needs next source to play. If you set ``autoplay`` as"
+" ``True``, the related source will be put after this event."
+msgstr ""
+"플레이어가 다음 소스를 필요로 할 때 호출됩니다. ``autoplay``를 ``True``로 설정하면 추천 영상이 이 이벤트 후에 "
+"추가됩니다."
+
+#: ../../events.rst:141
+msgid "current"
+msgstr "current"
+
+#: ../../events.rst:141 ../../events.rst:159 ../../events.rst:177
+msgid "AudioSource"
+msgstr "AudioSource"
+
+#: ../../events.rst:141
+msgid "The source which the player is currently playing"
+msgstr "플레이어가 현재 재생 중인 소스"
+
+#: ../../events.rst:145
+msgid "SOURCE_START(:py:class:`VoiceClient`, :py:class:`dict` data)"
+msgstr "SOURCE_START(:py:class:`VoiceClient`, :py:class:`dict` data)"
+
+#: ../../events.rst:147
+msgid "Called when the player starts to play the source."
+msgstr "플레이어가 소스를 재생하기 시작하면 호출됩니다."
+
+#: ../../events.rst:159
+msgid "The source which the player starts to play"
+msgstr "플레이어가 재생하기 시작하는 소스"
+
+#: ../../events.rst:163
+msgid "SOURCE_STOP(:py:class:`VoiceClient`, :py:class:`dict` data)"
+msgstr "SOURCE_STOP(:py:class:`VoiceClient`, :py:class:`dict` data)"
+
+#: ../../events.rst:165
+msgid "Called when the player stops to play the source."
+msgstr "플레이어가 재생중인 소스를 중지하면 호출됩니다."
+
+#: ../../events.rst:177
+msgid "The source which the player stops to play"
+msgstr "재생이 중지되는 소스"
+
+#: ../../events.rst:181
+msgid "PLAYER_TRACEBACK(:py:class:`VoiceClient`, :py:class:`dict` data)"
+msgstr "PLAYER_TRACEBACK(:py:class:`VoiceClient`, :py:class:`dict` data)"
+
+#: ../../events.rst:183
+msgid ""
+"Called when the player gets some traceback while trying to send packets to "
+"discord server."
+msgstr "플레이어가 디스코드 서버로 패킷을 보내려고 하는 동안 예외가 발생하면 호출됩니다."
+
+#: ../../events.rst:195 ../../events.rst:215
+msgid "traceback"
+msgstr "traceback"
+
+#: ../../events.rst:195 ../../events.rst:215
+msgid "The traceback information which the player gets"
+msgstr "발생한 예외 정보"
+
+#: ../../events.rst:199
+msgid "SOURCE_TRACEBACK(:py:class:`VoiceClient`, :py:class:`dict` data)"
+msgstr "SOURCE_TRACEBACK(:py:class:`VoiceClient`, :py:class:`dict` data)"
+
+#: ../../events.rst:201
+msgid ""
+"Called when the player gets some traceback while trying to play the source. "
+"That source will be automatically removed from the queue."
+msgstr "플레이어에서 소스를 재생하려고 시도하는 동안 예외가 발생하면 호출됩니다. 해당 소스는 대기열에서 자동으로 제거됩니다."
+
+#: ../../events.rst:213
+msgid "Union[AudioData, AudioSource]"
+msgstr "Union[AudioData, AudioSource]"
+
+#: ../../events.rst:213
+msgid "The source which the player gets traceback while trying to play"
+msgstr "플레이어가 재생을 시도하는 동안 예외가 발생한 소스"
diff --git a/docs/locale/ko/LC_MESSAGES/high_api.po b/docs/locale/ko/LC_MESSAGES/high_api.po
new file mode 100644
index 00000000..31e99ff4
--- /dev/null
+++ b/docs/locale/ko/LC_MESSAGES/high_api.po
@@ -0,0 +1,1348 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2021, kijk2869
+# This file is distributed under the same license as the discodo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: discodo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-03-07 18:34+0900\n"
+"PO-Revision-Date: 2021-03-06 02:55+0000\n"
+"Language-Team: Korean (https://www.transifex.com/mary-2/teams/117665/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../high_api.rst:4
+msgid "High Level API"
+msgstr ""
+
+#: ../../high_api.rst:6
+msgid "This section outlines the API of discodo."
+msgstr ""
+
+#: ../../high_api.rst:10
+msgid ""
+"This module uses the Python logging module to log diagnostic and errors in "
+"an output independent way. If the logging module is not configured, logs "
+"will not be output anywhere. See :ref:`logging_setup` for more information."
+msgstr ""
+
+#: ../../high_api.rst:16
+msgid "Client"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient:1
+msgid ""
+"Represents a client connection that connects to Discodo. This class will "
+"interact with Discodo nodes."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.connect:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.destroy:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.disconnect:0
+#: ../../../discodo/client/DPYClient.pydocstring of discodo.DPYClient.event:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getVC:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getWebsocket:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.dispatch:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.event:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.off:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.offAny:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.on:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.onAny:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.wait_for:0
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.discordDispatch:0
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.getVC:0 ../../../discodo/client/node.pydocstring
+#: of discodo.client.node.Node.send:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.fetchQueue:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSource:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSubtitle:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.loadSource:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.moveTo:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.putSource:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.query:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.requestSubtitle:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.searchSources:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.seek:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.send:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setAutoplay:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setContext:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setCrossfade:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setFilter:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setOptions:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setVolume:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.skip:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.moveTo:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.seek:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.setContext:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource.setContext:0
+msgid "Parameters"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient:4
+msgid "The client of the bot with discord.py"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher:0
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:0 ../../../discodo/errors.pydocstring of
+#: discodo.errors.HTTPException:0
+msgid "Variables"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient:6
+msgid "The list of :py:class:`discodo.Node` that is registered."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient:7
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:5
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient:9
+msgid "The event dispatcher that the client dispatches events."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient:8
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:6
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient:5
+msgid "The event loop that the client uses for operation."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.connect:1
+msgid "Connect to the voice channel."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.connect:3
+msgid "The channel to connect to."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.connect:4
+msgid "The node to connect with, defaults to ``getBestNode()``"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.connect:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.destroy:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getVC:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.wait_for:0
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.connect:0 ../../../discodo/client/node.pydocstring
+#: of discodo.client.node.Node.getVC:0
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node.send:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.query:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.put:0
+msgid "Raises"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.connect:6
+msgid "The ``channel`` value has no ``guild`` property."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.connect:7
+msgid "There is no discodo node that is connected."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.connect:8
+msgid "The connection is not established in 10 seconds."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.connect:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getBestNode:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getVC:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getWebsocket:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:0
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.DPYClient.voiceClients:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.off:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.offAny:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.on:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.onAny:0
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.wait_for:0
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getCpuCount:0
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getMemory:0 ../../../discodo/utils/status.pydocstring
+#: of discodo.utils.status.getNetworkInbound:0
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getNetworkOutbound:0
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getProcessCpu:0
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getProcessMemory:0
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getProcessThreads:0
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getStatus:0 ../../../discodo/utils/status.pydocstring
+#: of discodo.utils.status.getTotalCpu:0
+#: ../../../discodo/utils/tcp.pydocstring of discodo.utils.tcp.getFreePort:0
+#: ../../../discodo/client/node.pydocstring of discodo.Node.URL:0
+#: ../../../discodo/client/node.pydocstring of discodo.Node.WS_URL:0
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.getStatus:0
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.getVC:0 ../../../discodo/client/node.pydocstring
+#: of discodo.Node.is_connected:0 ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Nodes.connect:0
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Nodes.getStatus:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.autoplay:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.crossfade:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.fetchQueue:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.filter:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getContext:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getOptions:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSource:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getState:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSubtitle:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.loadSource:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.putSource:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.query:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.requestSubtitle:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.searchSources:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setAutoplay:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setContext:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setCrossfade:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setFilter:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setOptions:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setVolume:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.shuffle:0
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.volume:0 ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.getContext:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.moveTo:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.put:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.remove:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.seek:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.setContext:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource.getContext:0
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource.setContext:0
+msgid "Return type"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.destroy:1
+msgid "Destroy the voice client and disconnect from the voice channel"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.destroy:3
+msgid "The guild to destroy the voice client."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.destroy:5
+msgid "The voice client was not found."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.disconnect:1
+msgid "Disconnect from the voice channel."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.disconnect:4
+msgid ""
+"This coroutine doesn't destroy the voice client. Recommand to use "
+":meth:`destroy`"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.disconnect:6
+msgid "The guild to disconnect from."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of discodo.DPYClient.event:1
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.event:1
+msgid "A decorator that registers an event to listen to."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of discodo.DPYClient.event:3
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.event:3
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.on:3
+msgid "The event name to listen to."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getBestNode:1
+msgid "Get the node with the fewest connected players."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getVC:1
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.getVC:1
+msgid "Get a voice client from the guild."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getVC:3
+msgid "Guild or guild ID from which to get the voice client."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getVC:5
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.getVC:4
+msgid ""
+"Whether to raise an exception when the voice client cannot be found, "
+"defaults to ``False``."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getVC:7
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.getVC:6
+msgid "The voice client was not found and the ``safe`` value is ``False``."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getWebsocket:1
+msgid "Get a websocket object of the shard from discord.py"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.getWebsocket:3
+msgid "The shard id to get a object."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:1
+msgid ""
+"Creates a websocket connection of the node and register it on the client."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:3
+msgid ""
+"If the value ``host`` or ``port`` is ``None``, it will launch local node "
+"process."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:5
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:7
+msgid "The host of the node to connect to."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:6
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:8
+msgid "The port of the node to connect to."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:7
+msgid "The password of the node to connect to, defaults to ``hellodiscodo``."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:8
+msgid ""
+"The region of the node to connect to. This is like a annotation. It is not "
+"involved in the actual connection."
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:9
+msgid "The options to use when launching a local node"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:0
+msgid "Returns"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.client.DPYClient.DPYClient.registerNode:11
+msgid "The scheduled task to connect with the node"
+msgstr ""
+
+#: ../../../discodo/client/DPYClient.pydocstring of
+#: discodo.DPYClient.voiceClients:1
+msgid "A property that returns all voice clients from all of connected nodes."
+msgstr ""
+
+#: ../../high_api.rst:22
+msgid "Utils"
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher:1
+msgid "Represents an event dispatcher similar to EventEmitter_"
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher:5
+msgid ""
+"The event loop that the dispatcher uses for operation, defaults to "
+":py:meth:`asyncio.get_event_loop`"
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.dispatch:1
+msgid "Call the listeners which is matched with event name."
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.dispatch:3
+msgid "The event name to dispatch."
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.dispatch:4
+msgid "An argument list of data to dispatch with."
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.dispatch:5
+msgid "A keyword argument list of data to dispatch with."
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.off:1
+msgid "Remove the ``func`` function from the listeners list of the ``event``"
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.off:3
+msgid "The event name to remove from."
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.off:4
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.offAny:3
+msgid "The function to remove from the list."
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.offAny:1
+msgid "Remove the ``func`` function from the listeners list"
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.on:1
+msgid ""
+"Adds the ``func`` function to the end of the listeners list of the ``event``"
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.on:4
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.onAny:3
+msgid "The function to call when event dispatching."
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.onAny:1
+msgid "Adds the ``func`` function to the end of the listeners list"
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.wait_for:1
+msgid ""
+"Waits for an event that is matching with ``condition`` to be dispatched for "
+"``timeout``"
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.wait_for:3
+msgid "The event name to wait for"
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.wait_for:4
+msgid ""
+"A predicate to check what to wait for. this function must return ``bool``"
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.wait_for:5
+msgid "The number of seconds to wait."
+msgstr ""
+
+#: ../../../discodo/utils/eventDispatcher.pydocstring of
+#: discodo.utils.eventDispatcher.EventDispatcher.wait_for:7
+msgid "The timeout is provided and it was reached."
+msgstr ""
+
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getCpuCount:1
+msgid "Get cpu core count."
+msgstr ""
+
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getMemory:1
+msgid "Get system memory usage. the unit is mega bytes."
+msgstr ""
+
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getNetworkInbound:1
+msgid "Get network inbound counters. the unit is mega bytes."
+msgstr ""
+
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getNetworkOutbound:1
+msgid "Get network outbound counters. the unit is mega bytes."
+msgstr ""
+
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getProcessCpu:1
+msgid "Get process cpu usage. the unit is percent."
+msgstr ""
+
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getProcessMemory:1
+msgid "Get process memory usage. the unit is mega bytes."
+msgstr ""
+
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getProcessThreads:1
+msgid "Get process thread count."
+msgstr ""
+
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getStatus:1
+msgid "Represents all information to dictionary"
+msgstr ""
+
+#: ../../../discodo/utils/status.pydocstring of
+#: discodo.utils.status.getTotalCpu:1
+msgid "Get system cpu usage. the unit is percent."
+msgstr ""
+
+#: ../../../discodo/utils/callbackList.pydocstring of
+#: discodo.utils.callbackList.CallbackList:1
+msgid "A list that has callback when the value is changed."
+msgstr ""
+
+#: ../../../discodo/utils/callbackList.pydocstring of
+#: discodo.utils.callbackList.CallbackList.callback:1
+msgid "Override this function to use callback"
+msgstr ""
+
+#: ../../../discodo/utils/tcp.pydocstring of discodo.utils.tcp.getFreePort:1
+msgid "Get free port from the system."
+msgstr ""
+
+#: ../../high_api.rst:36
+msgid "Node"
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:1
+msgid "Represents a discodo node connection."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:3
+msgid "The client which the node is binded."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:4
+msgid "The websocket gateway the client is currently connected to."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:9
+msgid "The password of the node to connect to."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:10
+msgid "This bot's ID"
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:11
+msgid "This bot's shard ID, could be ``None``"
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:12
+msgid "Region set when registering a node"
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node:13
+msgid "A dictionary consisting of pairs of guild IDs and voice clients."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.Node.URL:1
+msgid "Represents the restful api url of the node."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.Node.WS_URL:1
+msgid "Represents the websocket url of the node."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.close:1
+msgid "some action to do after disconnected from node"
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.connect:1
+msgid "Connect to the node."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.connect:3
+msgid "The node is already connected."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.destroy:1
+msgid "Destroy the node and remove from the client."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.discordDispatch:1
+msgid "Dispatch the discord payload to the node."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.discordDispatch:4
+msgid ""
+"If you are using :py:class:`discodo.DPYClient`, you don't have to use this."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.discordDispatch:6
+msgid "The event data from the discord."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.getStatus:1
+msgid "Get status like cpu usage from the node with websocket."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Node.getVC:3
+msgid "guild ID from which to get the voice client."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.Node.is_connected:1
+msgid "Represents whether the node is connected."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node.send:1
+msgid "Send websocket payload to the node"
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node.send:3
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.query:3
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.send:3
+msgid "Operation name of the payload"
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node.send:4
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.query:4
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.send:4
+msgid "Operation data to send with"
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Node.send:6
+msgid "The node is not connnected."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Nodes:1
+msgid "Represents a discodo node connection list."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of discodo.client.node.Nodes:3
+msgid "You can also use it like :py:class:`list`."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Nodes.connect:1
+msgid "Try to connect to all registered nodes."
+msgstr ""
+
+#: ../../../discodo/client/node.pydocstring of
+#: discodo.client.node.Nodes.getStatus:1
+msgid "Try to get status from all registered nodes."
+msgstr ""
+
+#: ../../high_api.rst:45
+msgid "VoiceClient"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient:1
+msgid "Represents a voice connection of the guild."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient:3
+msgid "The node which the connection is connected with."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient:4
+msgid "The client which the connection is binded."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient:6
+msgid "The id of the voice client, which is used on restful api."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient:7
+msgid "The guild id which is connected to."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient:8
+msgid "The channel id which is connected to."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient:10
+msgid "The queue of the guild, it is synced to node and readonly."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.autoplay:1
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.filter:1
+msgid "Represents the autoplay state of this guild."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.crossfade:1
+msgid "Represents the crossfade duration of this guild."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.destroy:1
+msgid "Destroy the client"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.fetchQueue:1
+msgid "Fetch queue to force refresh the internal queue."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.fetchQueue:3
+msgid "Whether to request queue on websocket or not."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.filter:4
+msgid "For more information, check the documentation of the `FFmpeg Filter`_"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getContext:1
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.getContext:1
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource.getContext:1
+msgid "Get the context from the node."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getOptions:1
+msgid "Get options of the player"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSource:1
+msgid "Search the query and get source from extractor"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSource:3
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.loadSource:3
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.searchSources:3
+msgid "The query to search."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getState:1
+msgid "Fetch current player state."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSubtitle:1
+msgid ""
+"Request to send synced subtitle to discodo node and handle event to callback"
+" function."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSubtitle:3
+msgid "``lang`` or ``url`` is required."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSubtitle:5
+msgid "The callback function on subtitle event, must be coroutine function."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSubtitle:6
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.requestSubtitle:5
+msgid "The language to get subtitle."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.getSubtitle:7
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.requestSubtitle:6
+msgid "The subtitle url to fetch."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.loadSource:1
+msgid "Search the query and put source to the queue"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.moveTo:1
+msgid "Move the player's current Node."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.moveTo:3
+msgid "The node to move to."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.pause:1
+msgid "Pause the player"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.putSource:1
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.searchSources:1
+msgid "Search the query and get sources from extractor"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.putSource:3
+msgid "The source to put on the queue."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.query:1
+msgid "Send websocket payload to the node with guild id and await response."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.query:5
+msgid "Event name to receive response, defaults to ``op``"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.query:6
+msgid "Seconds to wait for response"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.query:8
+msgid "The query is timed out."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.query:9
+msgid "The node returned some exceptions."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.requestSubtitle:1
+msgid "Request to send synced subtitle to discodo node."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.requestSubtitle:3
+msgid "One of the parameters is required."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.resume:1
+msgid "Resume the player"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.seek:1
+msgid "Seek the player"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.seek:3
+msgid "The position to seek"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.send:1
+msgid "Send websocket payload to the node with guild id"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setAutoplay:1
+msgid "Set autoplay state of the player"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setAutoplay:3
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setOptions:5
+msgid "The autoplay state of the player to change."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setContext:1
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.setContext:1
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource.setContext:1
+msgid "Set the context to the node."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setContext:3
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.setContext:3
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource.setContext:3
+msgid "The context to set."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setCrossfade:1
+msgid "Set crossfade of the player"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setCrossfade:3
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setOptions:4
+msgid "The crossfade of the player to change."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setFilter:1
+msgid "Set filter of the player"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setFilter:3
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setOptions:6
+msgid "The filter object of the player to change."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setOptions:1
+msgid "Set options of the player"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setOptions:3
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setVolume:3
+msgid "The volume of the player to change."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.setVolume:1
+msgid "Set volume of the player"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.shuffle:1
+msgid "Shuffle the queue"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.skip:1
+msgid "Skip the source"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.client.voice_client.VoiceClient.skip:3
+msgid "how many to skip the sources"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.volume:1
+msgid "Represents the volume of this guild."
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.volume:3
+msgid "The maximum value is ``2.0``"
+msgstr ""
+
+#: ../../../discodo/client/voice_client.pydocstring of
+#: discodo.VoiceClient.volume:6
+msgid ""
+"This value is a percentage with decimal points. For example, ``100%`` is "
+"``1.0``, and ``5%`` is ``0.05``."
+msgstr ""
+
+#: ../../high_api.rst:51
+msgid "AudioData"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:1
+msgid "Object with playback information"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:7
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:7
+msgid "Checks if two AudioSources are equal."
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:11
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:11
+msgid "Checks if two AudioSources are not equal."
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:13
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:13
+msgid "The tag of the object"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:14
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:14
+msgid "The id of the source"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:15
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:15
+msgid "The title of the source"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:16
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:16
+msgid "The webpage url of the source"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:17
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:17
+msgid "The thumbnail url of the source"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:18
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:18
+msgid "The stream url of the source"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:19
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:19
+msgid "The duration of the source"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:20
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:20
+msgid "Whether the source is live stream or not"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:21
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:21
+msgid "The uploader of the source"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:22
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:23
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:24
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:22
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:23
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:24
+msgid "The description of the source"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:25
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:25
+msgid "Whether this source is added by autoplay"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:26
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:26
+msgid "The context of the object"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData:27
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:27
+msgid "The start position of the source"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.moveTo:1
+msgid "Move this source to index in the queue."
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.moveTo:3
+msgid "The index to move to"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.put:1
+msgid "Put the source into the queue."
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.put:3
+msgid "The source is already in the queue."
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.remove:1
+msgid "Remove this source from the queue."
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.seek:1
+msgid "Seek this source to offset."
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioData.seek:3
+msgid "The position to start playing"
+msgstr ""
+
+#: ../../high_api.rst:57
+msgid "AudioSource"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:1
+msgid "Object with playback information that is loaded"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:28
+msgid "Whether the source is seekable or not"
+msgstr ""
+
+#: ../../../discodo/client/models.pydocstring of
+#: discodo.client.models.AudioSource:29
+msgid "The current position of the source"
+msgstr ""
+
+#: ../../high_api.rst:63
+msgid "Errors"
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.DiscodoException:1
+msgid "The basic exception class of discodo"
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of
+#: discodo.errors.EncryptModeNotReceived:1
+msgid ""
+"Exception that is thrown when trying to send packet before receveing encrypt"
+" mode."
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of
+#: discodo.errors.EncryptModeNotReceived:3
+msgid "It's only raise in :py:class:`DiscordVoiceClient`"
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.NotPlaying:1
+msgid ""
+"Exception that is thrown when trying to operate something while not playing."
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.VoiceClientNotFound:1
+msgid "Exception that is thrown when there is no voice client."
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.NoSearchResults:1
+msgid "Exception that is thrown when there is no search results."
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.OpusLoadError:1
+msgid "Exception that is thrown when loading libopus failed."
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.HTTPException:1
+msgid "Exception that is thrown when HTTP operation failed."
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.HTTPException:3
+msgid "HTTP status code"
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.HTTPException:4
+msgid "Description of the HTTP status code"
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.HTTPException:5
+msgid "Server message with this request"
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.Forbidden:1
+msgid "Exception that is thrown when HTTP status code is 403."
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.TooManyRequests:1
+msgid "Exception that is thrown when HTTP status code is 429."
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.NotSeekable:1
+msgid ""
+"Exception that is thrown when trying to seek the source which is not "
+"seekable."
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.NodeException:1
+msgid "Exception that is thrown when discodo node returns some exception."
+msgstr ""
+
+#: ../../../discodo/errors.pydocstring of discodo.errors.NodeNotConnected:1
+msgid ""
+"Exception that is thrown when there is no discodo node that is connected."
+msgstr ""
diff --git a/docs/locale/ko/LC_MESSAGES/index.po b/docs/locale/ko/LC_MESSAGES/index.po
new file mode 100644
index 00000000..b02a5a10
--- /dev/null
+++ b/docs/locale/ko/LC_MESSAGES/index.po
@@ -0,0 +1,86 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2021, kijk2869
+# This file is distributed under the same license as the discodo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# Translators:
+# Mary <mareowst@gmail.com>, 2021
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: discodo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-03-07 18:34+0900\n"
+"PO-Revision-Date: 2021-02-28 08:54+0000\n"
+"Last-Translator: Mary <mareowst@gmail.com>, 2021\n"
+"Language-Team: Korean (https://www.transifex.com/mary-2/teams/117665/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../index.rst:40 ../../index.rst:40
+msgid "Introduction"
+msgstr "소개"
+
+#: ../../index.rst:48 ../../index.rst:48
+msgid "Node"
+msgstr "노드"
+
+#: ../../index.rst:57 ../../index.rst:57
+msgid "API Reference"
+msgstr "API 레퍼런스"
+
+#: ../../index.rst:7
+msgid "Welcome to discodo's documentation!"
+msgstr "discodo 문서에 오신 것을 환영합니다!"
+
+#: ../../index.rst:9
+msgid "Discodo is an enhanced audio player for discord."
+msgstr "Discodo는 디스코드를 위한 강화된 오디오 플레이어입니다."
+
+#: ../../index.rst:12
+msgid "Features"
+msgstr "기능들"
+
+#: ../../index.rst:14
+msgid "Standalone Audio Node"
+msgstr "독립형 오디오 노드"
+
+#: ../../index.rst:15
+msgid "Youtube Related Video Autoplay"
+msgstr "유튜브 추천 영상 자동 재생"
+
+#: ../../index.rst:16
+msgid "Crossfade and Audio effects"
+msgstr "크로스페이드와 음향 효과"
+
+#: ../../index.rst:17
+msgid "Synced Youtube Video Subtitle"
+msgstr "유튜브 영상 싱크 자막"
+
+#: ../../index.rst:20
+msgid "Supported sources"
+msgstr "지원되는 형식"
+
+#: ../../index.rst:22
+msgid "All sources that can be extracted from youtube-dl_"
+msgstr "youtube-dl_ 로 추출될 수 있는 모든 출처"
+
+#: ../../index.rst:23
+msgid "All formats that can be demuxed by libav_"
+msgstr "libav_ 로 디묵싱될 수 있는 모든 포멧"
+
+#: ../../index.rst:29
+msgid "Client libraries"
+msgstr "클라이언트 라이브러리"
+
+#: ../../index.rst:31
+msgid "discodo_ (Python_)"
+msgstr "discodo_ (Python_)"
+
+#: ../../index.rst:32
+msgid "discodo.js_ (Under Developing) (Node.js_)"
+msgstr "discodo.js_ (개발 중) (Node.js_)"
diff --git a/docs/locale/ko/LC_MESSAGES/introduction/installation.po b/docs/locale/ko/LC_MESSAGES/introduction/installation.po
new file mode 100644
index 00000000..db6d49bc
--- /dev/null
+++ b/docs/locale/ko/LC_MESSAGES/introduction/installation.po
@@ -0,0 +1,130 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2021, kijk2869
+# This file is distributed under the same license as the discodo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# Translators:
+# Mary <mareowst@gmail.com>, 2021
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: discodo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-02-28 16:55+0900\n"
+"PO-Revision-Date: 2021-02-28 08:54+0000\n"
+"Last-Translator: Mary <mareowst@gmail.com>, 2021\n"
+"Language-Team: Korean (https://www.transifex.com/mary-2/teams/117665/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../introduction/installation.rst:2
+msgid "Installation"
+msgstr "설치"
+
+#: ../../introduction/installation.rst:5
+msgid "Prerequisites"
+msgstr "필수 조건"
+
+#: ../../introduction/installation.rst:7
+msgid "Discodo works with **Python 3.7 or higher.**"
+msgstr "Discodo는 **Python 3.7 이상** 에서 작동합니다."
+
+#: ../../introduction/installation.rst:9
+msgid "Ealier versions of Python may not be worked."
+msgstr "더 오래된 버전의 파이썬은 작동하지 않을 수 있습니다."
+
+#: ../../introduction/installation.rst:12
+msgid "Dependencies"
+msgstr "의존성"
+
+#: ../../introduction/installation.rst:14
+msgid "On Linux environments, below dependencies are required:"
+msgstr "Linux 환경에서는 아래 종속성이 필요합니다:"
+
+#: ../../introduction/installation.rst:16
+msgid "``python3-dev``"
+msgstr "``python3-dev``"
+
+#: ../../introduction/installation.rst:17
+msgid "``libopus-dev``"
+msgstr "``libopus-dev``"
+
+#: ../../introduction/installation.rst:18
+msgid "``libnacl-dev``"
+msgstr "``libnacl-dev``"
+
+#: ../../introduction/installation.rst:20
+msgid "PyAV depends upon several libraries from FFmpeg:"
+msgstr "PyAV는 FFmpeg의 여러 라이브러리에 의존합니다:"
+
+#: ../../introduction/installation.rst:22
+msgid "``libavcodec-dev``"
+msgstr "``libavcodec-dev``"
+
+#: ../../introduction/installation.rst:23
+msgid "``libavdevice-dev``"
+msgstr "``libavdevice-dev``"
+
+#: ../../introduction/installation.rst:24
+msgid "``libavfilter-dev``"
+msgstr "``libavfilter-dev``"
+
+#: ../../introduction/installation.rst:25
+msgid "``libavformat-dev``"
+msgstr "``libavformat-dev``"
+
+#: ../../introduction/installation.rst:26
+msgid "``libavutil-dev``"
+msgstr "``libavutil-dev``"
+
+#: ../../introduction/installation.rst:27
+msgid "``libswresample-dev``"
+msgstr "``libswresample-dev``"
+
+#: ../../introduction/installation.rst:28
+msgid "``libswscale-dev``"
+msgstr "``libswscale-dev``"
+
+#: ../../introduction/installation.rst:29
+msgid "``pkg-config``"
+msgstr "``pkg-config``"
+
+#: ../../introduction/installation.rst:32
+msgid "Mac OS X"
+msgstr "Mac OS X"
+
+#: ../../introduction/installation.rst:39
+msgid "Ubuntu"
+msgstr "Ubuntu"
+
+#: ../../introduction/installation.rst:54
+msgid "Installing"
+msgstr "설치하기"
+
+#: ../../introduction/installation.rst:57
+msgid "PyPI"
+msgstr "PyPI"
+
+#: ../../introduction/installation.rst:64
+msgid "Docker"
+msgstr "Docker"
+
+#: ../../introduction/installation.rst:71
+msgid "Execution"
+msgstr "실행하기"
+
+#: ../../introduction/installation.rst:73
+msgid "You can see additional options with the ``--help`` flag."
+msgstr "``--help`` 인자를 통해 추가적인 옵션들을 볼 수 있습니다."
+
+#: ../../introduction/installation.rst:85
+msgid "Options"
+msgstr "설정"
+
+#: ../../introduction/installation.rst:134
+msgid "Config file"
+msgstr "설정 파일"
diff --git a/docs/locale/ko/LC_MESSAGES/introduction/logging.po b/docs/locale/ko/LC_MESSAGES/introduction/logging.po
new file mode 100644
index 00000000..47296e73
--- /dev/null
+++ b/docs/locale/ko/LC_MESSAGES/introduction/logging.po
@@ -0,0 +1,87 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2021, kijk2869
+# This file is distributed under the same license as the discodo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# Translators:
+# Mary <mareowst@gmail.com>, 2021
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: discodo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-02-28 16:55+0900\n"
+"PO-Revision-Date: 2021-02-28 08:54+0000\n"
+"Last-Translator: Mary <mareowst@gmail.com>, 2021\n"
+"Language-Team: Korean (https://www.transifex.com/mary-2/teams/117665/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../introduction/logging.rst:2
+msgid "Setting Up Logging"
+msgstr "로깅 설정하기"
+
+#: ../../introduction/logging.rst:5
+msgid "Basic logging"
+msgstr "기본 로깅"
+
+#: ../../introduction/logging.rst:7
+msgid ""
+"discodo_ logs several information via the logging_ python module like "
+"discord.py_. It is strongly recommended to configure the logging module, as "
+"you can't see some error if it is not set up. You can configure the logging_"
+" module as simple as:"
+msgstr ""
+"discodo_ 는 파이썬 logging_ 모듈을 통해 여러 정보를 기록합니다. 설정하지 않는다면 일부 에러를 볼 수 없기 때문에, 로깅"
+" 모듈을 설정하는 것은 강력하게 권장됩니다. logging_ 모듈은 다음과 같이 간단하게 설정할 수 있습니다:"
+
+#: ../../introduction/logging.rst:20
+msgid ""
+"Placed at the start of the code. This will output the logs from all "
+"libraries which use the logging_ module, including discodo_, to the console."
+msgstr ""
+"코드의 시작 부분에 두십시오. 이것은 discodo_ 를 포함한 logging_ 모듈을 사용하는 모든 라이브러리로부터 로그를 콘솔에 "
+"출력할 것입니다."
+
+#: ../../introduction/logging.rst:22
+msgid ""
+"The ``level`` argument specifies what level of events to log and can be any "
+"of ``CRITICAL``, ``ERROR``, ``WARNING``, ``INFO``, and ``DEBUG`` and default"
+" value is ``WARNING``"
+msgstr ""
+"``level`` 인자는 기록할 이벤트의 수준을 정의하며, ``CRITICAL``, ``ERROR``, ``WARNING``, "
+"``INFO``, ``DEBUG`` 중 하나 일 수 있습니다. 기본 값은 ``WARNING`` 입니다."
+
+#: ../../introduction/logging.rst:24
+msgid "For more information, check the documentation of the logging_ module."
+msgstr "더 많은 정보를 얻으려면, logging_ 모듈의 문서를 참고하십시오."
+
+#: ../../introduction/logging.rst:27
+msgid "Use rich formatter"
+msgstr "Rich 포멧터 사용하기"
+
+#: ../../introduction/logging.rst:29
+msgid ""
+"Also, discodo_ uses rich_ to improve log readability. rich_ is an library "
+"for rich text and formatting in the terminal. To output the log to the "
+"terminal using rich_, you can set it as follows:"
+msgstr ""
+"또한, discodo_ 는 로그의 가독성을 향상시키기 위해 rich_ 를 사용합니다. rich_ 는 텍스트 및 콘솔 서식을 위한 "
+"라이브러리입니다. rich_ 를 사용하여 터미널에 로그를 출력하려면, 다음과 같이 설정하십시오:"
+
+#: ../../introduction/logging.rst:38
+msgid ""
+"Because rich_ displays the ``log level`` separately, remove the ``level`` "
+"from the ``format`` argument, and set ``rich_tracebacks`` to ``True`` for "
+"formatting tracebacks."
+msgstr ""
+"rich_ 가 ``log level`` 을 따로 표시하기 때문에, ``format`` 인자에서 ``level`` 을 제거하세요, 그리고 "
+"예외를 서식하기 위해 ``rich_tracebacks`` 를 ``True`` 로 설정하세요."
+
+#: ../../introduction/logging.rst:40
+msgid "When you set this, the log will be formatted as follows:"
+msgstr "이것을 설정하면 로그 형식은 다음과 같습니다:"
diff --git a/docs/locale/ko/LC_MESSAGES/introduction/quickstart.po b/docs/locale/ko/LC_MESSAGES/introduction/quickstart.po
new file mode 100644
index 00000000..9bc80098
--- /dev/null
+++ b/docs/locale/ko/LC_MESSAGES/introduction/quickstart.po
@@ -0,0 +1,107 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2021, kijk2869
+# This file is distributed under the same license as the discodo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# Translators:
+# Mary <mareowst@gmail.com>, 2021
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: discodo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-02-28 21:01+0900\n"
+"PO-Revision-Date: 2021-02-28 08:54+0000\n"
+"Last-Translator: Mary <mareowst@gmail.com>, 2021\n"
+"Language-Team: Korean (https://www.transifex.com/mary-2/teams/117665/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../introduction/quickstart.rst:2
+msgid "Quickstart"
+msgstr "빠른 시작"
+
+#: ../../introduction/quickstart.rst:5
+msgid "A Minimal Bot with discord.py_"
+msgstr "discord.py_ 를 사용한 작은 봇"
+
+#: ../../introduction/quickstart.rst:8
+msgid "Let’s make a bot which uses local node feature."
+msgstr "한번 로컬 노드를 사용하는 봇을 만들어봅시다."
+
+#: ../../introduction/quickstart.rst:10
+msgid "It looks like this:"
+msgstr "다음과 같습니다:"
+
+#: ../../introduction/quickstart.rst:71
+msgid "Let's name this file ``simple_musicbot.py``"
+msgstr "이 파일의 이름을 ``simple_musicbot.py`` 로 바꿔봅시다"
+
+#: ../../introduction/quickstart.rst:73
+msgid ""
+"Assume you know how to use discord.py_, and I will explain the discodo_ code"
+" step by step."
+msgstr "discord.py_ 사용법을 알고 있다고 가정하고, discodo_ 코드를 단계별로 설명하겠습니다."
+
+#: ../../introduction/quickstart.rst:77
+msgid ""
+"We create an instance of :py:class:`DPYClient`. This client will manage "
+"voice connections to Discord."
+msgstr ":py:class:`DPYClient` 인스턴스를 생성합니다. 이 클라이언트는 디스코드와의 보이스 연결을 관리할 것입니다."
+
+#: ../../introduction/quickstart.rst:78
+msgid ""
+"After ``on_ready`` event, we use the :py:attr:`DPYClient.event()` decorator "
+"to register an event like discord.py_ . In this case, ``SOURCE_START`` will "
+"be called when the music starts playing."
+msgstr ""
+"``on_ready`` 이벤트 이후에, 우리는 discord.py_ 와 비슷하게 :py:attr:`DPYClient.event()` "
+"데코레이터를 사용하여 이벤트를 등록합니다. 이 경우에는, ``SOURCE_START`` 는 음악 재생이 시작되었을 때 호출될 것입니다."
+
+#: ../../introduction/quickstart.rst:79
+msgid ""
+"When the ``!join`` command is excuted, we check if the "
+":py:attr:`discord.Message.author` is connected to the voice channel. If it "
+"is, then we connected to the channel using :py:func:`DPYClient.connect()`"
+msgstr ""
+"``!join`` 커맨드가 실행됬을 때, :py:attr:`discord.Message.author` 이 보이스 채널에 접속했는지 "
+"확인합니다. 만약 그렇다면, :py:func:`DPYClient.connect()` 를 사용하여 그 채널에 접속합니다."
+
+#: ../../introduction/quickstart.rst:80
+msgid ""
+"When the ``!play`` command runs, set the VC.channel to the current message "
+"channel to send messages during playback, search for queries and add them to"
+" the list."
+msgstr ""
+"``!play`` 커맨드가 실행됬을 때, 재생 알림 메세지를 보내기 위해 VC.channel을 현재 메세지 채널로 설정하고, 인자를 "
+"검색하고 재생목록에 추가합니다."
+
+#: ../../introduction/quickstart.rst:81
+msgid ""
+"If the ``!stop`` command is excuted, we destroy the voice client via "
+":py:func:`VoiceClient.destroy()`"
+msgstr ""
+"만약 ``!stop`` 커맨드가 실행된다면, :py:func:`VoiceClient.destroy()` 를 통해 보이스 클라이언트를 "
+"제거합니다."
+
+#: ../../introduction/quickstart.rst:82
+msgid ""
+"Finally, we set local nodes to be used by not giving host argument to "
+":py:func:`DPYClient.registerNode()`"
+msgstr ""
+"마지막으로, :py:func:`DPYClient.registerNode()` 에 host 인자를 주지 않음으로서 로컬노드를 사용하도록 "
+"설정합니다."
+
+#: ../../introduction/quickstart.rst:84
+msgid ""
+"Now that we've made a simple music bot, we have to run this. Just as you do "
+"when you run a discord.py_ Bot"
+msgstr "이제 간단한 음악 봇을 만들었으므로, 이 코드를 실행해야합니다. discord.py_ 봇을 실행할 때 처럼 하면 됩니다."
+
+#: ../../introduction/quickstart.rst:90
+msgid "Now you can try playing around with your basic musicbot."
+msgstr "이제 이 간단한 뮤직 봇으로 놀아 볼 수 있습니다."
diff --git a/docs/locale/ko/LC_MESSAGES/node/connection.po b/docs/locale/ko/LC_MESSAGES/node/connection.po
new file mode 100644
index 00000000..a6404e07
--- /dev/null
+++ b/docs/locale/ko/LC_MESSAGES/node/connection.po
@@ -0,0 +1,179 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2021, kijk2869
+# This file is distributed under the same license as the discodo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# Translators:
+# Mary <mareowst@gmail.com>, 2021
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: discodo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-03-07 18:34+0900\n"
+"PO-Revision-Date: 2021-03-05 01:26+0000\n"
+"Last-Translator: Mary <mareowst@gmail.com>, 2021\n"
+"Language-Team: Korean (https://www.transifex.com/mary-2/teams/117665/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../node/connection.rst:2
+msgid "Websocket Connection"
+msgstr "웹소켓 연결"
+
+#: ../../node/connection.rst:4
+msgid "Discodo using websocket to send and receive events."
+msgstr "Discodo는 웹소켓을 사용하여 이벤트를 주고받습니다."
+
+#: ../../node/connection.rst:7
+msgid "Payloads"
+msgstr "페이로드"
+
+#: ../../node/connection.rst:10
+msgid "Payload Structure"
+msgstr "페이로드 구조"
+
+#: ../../node/connection.rst:13 ../../node/connection.rst:30
+msgid "Field"
+msgstr "이름"
+
+#: ../../node/connection.rst:13 ../../node/connection.rst:30
+msgid "Type"
+msgstr "자료형"
+
+#: ../../node/connection.rst:13 ../../node/connection.rst:30
+msgid "Description"
+msgstr "설명"
+
+#: ../../node/connection.rst:15
+msgid "op"
+msgstr "op"
+
+#: ../../node/connection.rst:15 ../../node/connection.rst:32
+msgid "string"
+msgstr "문자열"
+
+#: ../../node/connection.rst:15
+msgid "operation name for the payload"
+msgstr "페이로드의 작업 이름"
+
+#: ../../node/connection.rst:17
+msgid "d"
+msgstr "d"
+
+#: ../../node/connection.rst:17
+msgid "?mixed (Mostly JSON)"
+msgstr "?혼합 (대부분 JSON)"
+
+#: ../../node/connection.rst:17
+msgid "event data"
+msgstr "이벤트 데이터"
+
+#: ../../node/connection.rst:21
+msgid "Connecting to Discodo"
+msgstr "Discodo에 연결하기"
+
+#: ../../node/connection.rst:24
+msgid "Connecting"
+msgstr "연결하기"
+
+#: ../../node/connection.rst:27
+msgid "Websocket Headers"
+msgstr "웹소켓 헤더"
+
+#: ../../node/connection.rst:32
+msgid "Authorization"
+msgstr "Authorization"
+
+#: ../../node/connection.rst:32
+msgid "Password for discodo server"
+msgstr "discodo 서버의 비밀번호"
+
+#: ../../node/connection.rst:35
+msgid ""
+"Once connected, the client should immediately receive ``HELLO`` with the "
+"connection's heartbeat interval unless you missed headers or mismatched, "
+"otherwise receive ``FORBIDDEN``."
+msgstr ""
+"연결되면, 헤더가 일치하지 않는 한 클라이언트는 해당 연결의 heartbeat 주기를 포함한 ``HELLO`` 를 즉시 수신해야 합니다,"
+" 그렇지 않으면 ``FORBIDDEN`` 을 수신합니다."
+
+#: ../../node/connection.rst:38
+msgid "> Example HELLO"
+msgstr "> HELLO 예제"
+
+#: ../../node/connection.rst:50
+msgid "> Example FORBIDDEN"
+msgstr "> FORBIDDEN 예제"
+
+#: ../../node/connection.rst:60
+msgid "Heartbeating"
+msgstr "하트비트"
+
+#: ../../node/connection.rst:62
+msgid ""
+"Recieveing ``HELLO`` payload, the client should begin sending ``HEARTBEAT`` "
+"every ``heartbeat_interval`` seconds, until the connection closed."
+msgstr ""
+"``HELLO`` 를 수신하면,  클라이언트는 연결이 닫힐 때까지 매 ``heartbeat_interval`` 초 마다 "
+"``HEARTBEAT`` 를 전송하기 시작해야 합니다."
+
+#: ../../node/connection.rst:65
+msgid "< Example HEARTBEAT"
+msgstr "< HEARTBEAT 예제"
+
+#: ../../node/connection.rst:74
+msgid "Event Data (``d``) can be ``None``, the server will echo them."
+msgstr "이벤트 데이터 (``d``) 는 ``None`` 일 수 있습니다, 서버는 그것을 반환할 것입니다."
+
+#: ../../node/connection.rst:77
+msgid "> Example HEARTBEAT_ACK"
+msgstr "> HEARTBEAT_ACK 예제"
+
+#: ../../node/connection.rst:87
+msgid "Identifying"
+msgstr "식별"
+
+#: ../../node/connection.rst:89
+msgid ""
+"The client must send ``IDENTIFY`` to configure the audio manager before "
+"using."
+msgstr "클라이언트는 오디오 매니저를 사용하기 전에 설정하기 위해 ``IDENTIFY`` 를 전송해야합니다."
+
+#: ../../node/connection.rst:92
+msgid "< Example IDENTIFY"
+msgstr "< IDENTIFY 예제"
+
+#: ../../node/connection.rst:105
+msgid "Resumed"
+msgstr "재개"
+
+#: ../../node/connection.rst:107
+msgid ""
+"If the same ``user_id`` with the same ``shard_id`` is connected before "
+"``VC_TIMEOUT``, it will be resumed."
+msgstr ""
+"``VC_TIMEOUT`` 전에 같은 ``shard_id`` 를 가진 같은 ``user_id`` 가 연결된다면, 연결이 재개됩니다."
+
+#: ../../node/connection.rst:110
+msgid "> Example Resumed"
+msgstr "> Resumed 예제"
+
+#: ../../node/connection.rst:123
+msgid ""
+"When the client recieve ``RESUMED``, must reconnect to each voice channel."
+msgstr "클라이언트가 ``RESUMED`` 를 수신했을 때, 각 음성 채널에 다시 연결해야 합니다."
+
+#: ../../node/connection.rst:126
+msgid "Disconnecting"
+msgstr "연결 끊기"
+
+#: ../../node/connection.rst:128
+msgid ""
+"If the connection is closed, the server will clean up manager and sources "
+"after ``VC_TIMEOUT``"
+msgstr "만약 연결이 닫힌다면, 서버는 ``VC_TIMEOUT`` 후에 매니저와 소스들을 정리할 것입니다."
diff --git a/docs/locale/ko/LC_MESSAGES/node/resources/events.po b/docs/locale/ko/LC_MESSAGES/node/resources/events.po
new file mode 100644
index 00000000..8d0af430
--- /dev/null
+++ b/docs/locale/ko/LC_MESSAGES/node/resources/events.po
@@ -0,0 +1,559 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2021, kijk2869
+# This file is distributed under the same license as the discodo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: discodo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-03-09 10:43+0900\n"
+"PO-Revision-Date: 2021-03-07 09:35+0000\n"
+"Language-Team: Korean (https://www.transifex.com/mary-2/teams/117665/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../node/resources/events.rst:4
+msgid "Event Reference"
+msgstr ""
+
+#: ../../node/resources/events.rst:6
+msgid ""
+"This section outlines the different types of events dispatched by discodo "
+"node with websocket."
+msgstr ""
+
+#: ../../node/resources/events.rst:9
+msgid ""
+"If you are using :py:class:`DPYClient`, the events that you get will have "
+"something different. See this :ref:`client_events`."
+msgstr ""
+
+#: ../../node/resources/events.rst:12
+msgid "STATUS"
+msgstr ""
+
+#: ../../node/resources/events.rst:14
+msgid ""
+"Called when the client requests system information by ``getStatus``. the "
+"unit is mega bytes or percent."
+msgstr ""
+
+#: ../../node/resources/events.rst:17 ../../node/resources/events.rst:47
+#: ../../node/resources/events.rst:63 ../../node/resources/events.rst:74
+#: ../../node/resources/events.rst:89 ../../node/resources/events.rst:102
+#: ../../node/resources/events.rst:115 ../../node/resources/events.rst:128
+#: ../../node/resources/events.rst:141 ../../node/resources/events.rst:154
+#: ../../node/resources/events.rst:167 ../../node/resources/events.rst:194
+#: ../../node/resources/events.rst:207 ../../node/resources/events.rst:222
+#: ../../node/resources/events.rst:241 ../../node/resources/events.rst:254
+#: ../../node/resources/events.rst:267
+msgid "Field"
+msgstr ""
+
+#: ../../node/resources/events.rst:17 ../../node/resources/events.rst:47
+#: ../../node/resources/events.rst:63 ../../node/resources/events.rst:74
+#: ../../node/resources/events.rst:89 ../../node/resources/events.rst:102
+#: ../../node/resources/events.rst:115 ../../node/resources/events.rst:128
+#: ../../node/resources/events.rst:141 ../../node/resources/events.rst:154
+#: ../../node/resources/events.rst:167 ../../node/resources/events.rst:194
+#: ../../node/resources/events.rst:207 ../../node/resources/events.rst:222
+#: ../../node/resources/events.rst:241 ../../node/resources/events.rst:254
+#: ../../node/resources/events.rst:267
+msgid "Type"
+msgstr ""
+
+#: ../../node/resources/events.rst:17 ../../node/resources/events.rst:47
+#: ../../node/resources/events.rst:63 ../../node/resources/events.rst:74
+#: ../../node/resources/events.rst:89 ../../node/resources/events.rst:102
+#: ../../node/resources/events.rst:115 ../../node/resources/events.rst:128
+#: ../../node/resources/events.rst:141 ../../node/resources/events.rst:154
+#: ../../node/resources/events.rst:167 ../../node/resources/events.rst:194
+#: ../../node/resources/events.rst:207 ../../node/resources/events.rst:222
+#: ../../node/resources/events.rst:241 ../../node/resources/events.rst:254
+#: ../../node/resources/events.rst:267
+msgid "Description"
+msgstr ""
+
+#: ../../node/resources/events.rst:19
+msgid "UsedMemory"
+msgstr ""
+
+#: ../../node/resources/events.rst:19 ../../node/resources/events.rst:21
+#: ../../node/resources/events.rst:23 ../../node/resources/events.rst:25
+#: ../../node/resources/events.rst:27 ../../node/resources/events.rst:29
+#: ../../node/resources/events.rst:31 ../../node/resources/events.rst:33
+msgid "integer"
+msgstr ""
+
+#: ../../node/resources/events.rst:19
+msgid "The process memory usage"
+msgstr ""
+
+#: ../../node/resources/events.rst:21
+msgid "TotalMemory"
+msgstr ""
+
+#: ../../node/resources/events.rst:21
+msgid "The system memory usage"
+msgstr ""
+
+#: ../../node/resources/events.rst:23
+msgid "ProcessLoad"
+msgstr ""
+
+#: ../../node/resources/events.rst:23
+msgid "The process cpu usage"
+msgstr ""
+
+#: ../../node/resources/events.rst:25
+msgid "TotalLoad"
+msgstr ""
+
+#: ../../node/resources/events.rst:25
+msgid "The system cpu usage"
+msgstr ""
+
+#: ../../node/resources/events.rst:27
+msgid "Cores"
+msgstr ""
+
+#: ../../node/resources/events.rst:27
+msgid "The cpu core count"
+msgstr ""
+
+#: ../../node/resources/events.rst:29
+msgid "Threads"
+msgstr ""
+
+#: ../../node/resources/events.rst:29
+msgid "The process thread count"
+msgstr ""
+
+#: ../../node/resources/events.rst:31
+msgid "NetworkInbound"
+msgstr ""
+
+#: ../../node/resources/events.rst:31
+msgid "The network inbound counters"
+msgstr ""
+
+#: ../../node/resources/events.rst:33
+msgid "NetworkOutbound"
+msgstr ""
+
+#: ../../node/resources/events.rst:33
+msgid "The network outbound counters"
+msgstr ""
+
+#: ../../node/resources/events.rst:37
+msgid "HEARTBEAT_ACK"
+msgstr ""
+
+#: ../../node/resources/events.rst:39
+msgid ""
+"Called when the client send ``HEARTBEAT`` payload. The data of this event is"
+" payload data."
+msgstr ""
+
+#: ../../node/resources/events.rst:42
+msgid "IDENTIFIED"
+msgstr ""
+
+#: ../../node/resources/events.rst:44
+msgid ""
+"Called when the new voice client has successfully created. This is not the "
+"same as the client being fully connected."
+msgstr ""
+
+#: ../../node/resources/events.rst:49 ../../node/resources/events.rst:65
+#: ../../node/resources/events.rst:76 ../../node/resources/events.rst:91
+#: ../../node/resources/events.rst:104 ../../node/resources/events.rst:117
+#: ../../node/resources/events.rst:130 ../../node/resources/events.rst:143
+#: ../../node/resources/events.rst:156 ../../node/resources/events.rst:169
+#: ../../node/resources/events.rst:196 ../../node/resources/events.rst:209
+#: ../../node/resources/events.rst:224 ../../node/resources/events.rst:243
+#: ../../node/resources/events.rst:256 ../../node/resources/events.rst:269
+msgid "guild_id"
+msgstr ""
+
+#: ../../node/resources/events.rst:49 ../../node/resources/events.rst:65
+#: ../../node/resources/events.rst:76 ../../node/resources/events.rst:91
+#: ../../node/resources/events.rst:93 ../../node/resources/events.rst:104
+#: ../../node/resources/events.rst:117 ../../node/resources/events.rst:130
+#: ../../node/resources/events.rst:143 ../../node/resources/events.rst:156
+#: ../../node/resources/events.rst:183 ../../node/resources/events.rst:196
+#: ../../node/resources/events.rst:209 ../../node/resources/events.rst:224
+#: ../../node/resources/events.rst:243 ../../node/resources/events.rst:256
+#: ../../node/resources/events.rst:269
+msgid "int"
+msgstr ""
+
+#: ../../node/resources/events.rst:49 ../../node/resources/events.rst:76
+#: ../../node/resources/events.rst:91 ../../node/resources/events.rst:104
+#: ../../node/resources/events.rst:117 ../../node/resources/events.rst:130
+#: ../../node/resources/events.rst:143 ../../node/resources/events.rst:156
+#: ../../node/resources/events.rst:169 ../../node/resources/events.rst:196
+#: ../../node/resources/events.rst:209 ../../node/resources/events.rst:224
+#: ../../node/resources/events.rst:243 ../../node/resources/events.rst:256
+#: ../../node/resources/events.rst:269
+msgid "The guild id of the voice client"
+msgstr ""
+
+#: ../../node/resources/events.rst:51
+msgid "id"
+msgstr ""
+
+#: ../../node/resources/events.rst:51 ../../node/resources/events.rst:78
+#: ../../node/resources/events.rst:169 ../../node/resources/events.rst:171
+#: ../../node/resources/events.rst:173 ../../node/resources/events.rst:211
+#: ../../node/resources/events.rst:213 ../../node/resources/events.rst:226
+#: ../../node/resources/events.rst:228 ../../node/resources/events.rst:230
+#: ../../node/resources/events.rst:232 ../../node/resources/events.rst:245
+#: ../../node/resources/events.rst:258 ../../node/resources/events.rst:273
+msgid "str"
+msgstr ""
+
+#: ../../node/resources/events.rst:51
+msgid "The id of the voice client"
+msgstr ""
+
+#: ../../node/resources/events.rst:55
+msgid "VC_DESTROYED"
+msgstr ""
+
+#: ../../node/resources/events.rst:57
+msgid "Called when the voice client has successfully destroyed."
+msgstr ""
+
+#: ../../node/resources/events.rst:60
+msgid ""
+"This does not mean that the bot have disconnected from the voice channel. "
+"When the client receives this event, it should disconnect from the voice "
+"channel."
+msgstr ""
+
+#: ../../node/resources/events.rst:65
+msgid "The guild id of the voice client that is destroyed"
+msgstr ""
+
+#: ../../node/resources/events.rst:69
+msgid "QUEUE_EVENT"
+msgstr ""
+
+#: ../../node/resources/events.rst:71
+msgid ""
+"Called when there is something changed in the queue of the voice client."
+msgstr ""
+
+#: ../../node/resources/events.rst:78
+msgid "name"
+msgstr ""
+
+#: ../../node/resources/events.rst:78
+msgid "The name of the method"
+msgstr ""
+
+#: ../../node/resources/events.rst:80
+msgid "args"
+msgstr ""
+
+#: ../../node/resources/events.rst:80 ../../node/resources/events.rst:106
+#: ../../node/resources/events.rst:198
+msgid "list"
+msgstr ""
+
+#: ../../node/resources/events.rst:80
+msgid "The arguments of the method"
+msgstr ""
+
+#: ../../node/resources/events.rst:84
+msgid "VC_CHANNEL_EDITED"
+msgstr ""
+
+#: ../../node/resources/events.rst:86
+msgid "Called when the voice channel of the voice client is changed."
+msgstr ""
+
+#: ../../node/resources/events.rst:93 ../../node/resources/events.rst:171
+msgid "channel_id"
+msgstr ""
+
+#: ../../node/resources/events.rst:93 ../../node/resources/events.rst:171
+msgid "The channel id of the voice client"
+msgstr ""
+
+#: ../../node/resources/events.rst:97
+msgid "putSource"
+msgstr ""
+
+#: ../../node/resources/events.rst:99
+msgid "Called when some sources are put in the queue."
+msgstr ""
+
+#: ../../node/resources/events.rst:106
+msgid "sources"
+msgstr ""
+
+#: ../../node/resources/events.rst:106
+msgid "The sources which is put"
+msgstr ""
+
+#: ../../node/resources/events.rst:110
+msgid "loadSource"
+msgstr ""
+
+#: ../../node/resources/events.rst:112
+msgid "Called when some sources are searched and put in the queue."
+msgstr ""
+
+#: ../../node/resources/events.rst:119 ../../node/resources/events.rst:145
+#: ../../node/resources/events.rst:158 ../../node/resources/events.rst:271
+msgid "source"
+msgstr ""
+
+#: ../../node/resources/events.rst:119
+msgid "list or AudioData"
+msgstr ""
+
+#: ../../node/resources/events.rst:119
+msgid "The sources which is searched and put"
+msgstr ""
+
+#: ../../node/resources/events.rst:123
+msgid "REQUIRE_NEXT_SOURCE"
+msgstr ""
+
+#: ../../node/resources/events.rst:125
+msgid ""
+"Called when the player needs next source to play. If you set ``autoplay`` as"
+" ``True``, the related source will be put after this event."
+msgstr ""
+
+#: ../../node/resources/events.rst:132 ../../node/resources/events.rst:175
+#: ../../node/resources/events.rst:230
+msgid "current"
+msgstr ""
+
+#: ../../node/resources/events.rst:132 ../../node/resources/events.rst:145
+#: ../../node/resources/events.rst:158 ../../node/resources/events.rst:175
+msgid "AudioSource"
+msgstr ""
+
+#: ../../node/resources/events.rst:132
+msgid "The source which the player is currently playing"
+msgstr ""
+
+#: ../../node/resources/events.rst:136
+msgid "SOURCE_START"
+msgstr ""
+
+#: ../../node/resources/events.rst:138
+msgid "Called when the player starts to play the source."
+msgstr ""
+
+#: ../../node/resources/events.rst:145
+msgid "The source which the player starts to play"
+msgstr ""
+
+#: ../../node/resources/events.rst:149
+msgid "SOURCE_STOP"
+msgstr ""
+
+#: ../../node/resources/events.rst:151
+msgid "Called when the player stops to play the source."
+msgstr ""
+
+#: ../../node/resources/events.rst:158
+msgid "The source which the player stops to play"
+msgstr ""
+
+#: ../../node/resources/events.rst:162
+msgid "getState"
+msgstr ""
+
+#: ../../node/resources/events.rst:164
+msgid "Called when the client requests the player state by ``getState``."
+msgstr ""
+
+#: ../../node/resources/events.rst:173
+msgid "state"
+msgstr ""
+
+#: ../../node/resources/events.rst:173
+msgid "Current state of the voice client"
+msgstr ""
+
+#: ../../node/resources/events.rst:175
+msgid "Current source of the player"
+msgstr ""
+
+#: ../../node/resources/events.rst:177
+msgid "duration"
+msgstr ""
+
+#: ../../node/resources/events.rst:177 ../../node/resources/events.rst:179
+#: ../../node/resources/events.rst:181
+msgid "float"
+msgstr ""
+
+#: ../../node/resources/events.rst:177
+msgid "Current duration of the source that is playing"
+msgstr ""
+
+#: ../../node/resources/events.rst:179
+msgid "position"
+msgstr ""
+
+#: ../../node/resources/events.rst:179
+msgid "Current position of the source that is playing"
+msgstr ""
+
+#: ../../node/resources/events.rst:181
+msgid "remain"
+msgstr ""
+
+#: ../../node/resources/events.rst:181
+msgid "(duration value) - (position value)"
+msgstr ""
+
+#: ../../node/resources/events.rst:183
+msgid "remainQueue"
+msgstr ""
+
+#: ../../node/resources/events.rst:183
+msgid "Current queue length of the player"
+msgstr ""
+
+#: ../../node/resources/events.rst:185
+msgid "options"
+msgstr ""
+
+#: ../../node/resources/events.rst:185
+msgid "JSON"
+msgstr ""
+
+#: ../../node/resources/events.rst:185
+msgid "Current options of the player"
+msgstr ""
+
+#: ../../node/resources/events.rst:189
+msgid "getQueue"
+msgstr ""
+
+#: ../../node/resources/events.rst:191
+msgid "Called when the client requests the player queue by ``getQueue``."
+msgstr ""
+
+#: ../../node/resources/events.rst:198
+msgid "entries"
+msgstr ""
+
+#: ../../node/resources/events.rst:198
+msgid "The entries of the queue"
+msgstr ""
+
+#: ../../node/resources/events.rst:202
+msgid "requestSubtitle"
+msgstr ""
+
+#: ../../node/resources/events.rst:204
+msgid ""
+"Called when the client requests synced subtitles by ``requestSubtitle``."
+msgstr ""
+
+#: ../../node/resources/events.rst:211 ../../node/resources/events.rst:226
+#: ../../node/resources/events.rst:245
+msgid "identify"
+msgstr ""
+
+#: ../../node/resources/events.rst:211 ../../node/resources/events.rst:226
+#: ../../node/resources/events.rst:245
+msgid "The id to identify the subtitle"
+msgstr ""
+
+#: ../../node/resources/events.rst:213
+msgid "url"
+msgstr ""
+
+#: ../../node/resources/events.rst:213
+msgid "The url of the subtitle"
+msgstr ""
+
+#: ../../node/resources/events.rst:217
+msgid "Subtitle"
+msgstr ""
+
+#: ../../node/resources/events.rst:219
+msgid ""
+"This event is for sending the sync subtitle. This event is sent according to"
+" the player's position."
+msgstr ""
+
+#: ../../node/resources/events.rst:228
+msgid "previous"
+msgstr ""
+
+#: ../../node/resources/events.rst:228
+msgid "The content of previous subtitle"
+msgstr ""
+
+#: ../../node/resources/events.rst:230
+msgid "The content of current subtitle"
+msgstr ""
+
+#: ../../node/resources/events.rst:232
+msgid "next"
+msgstr ""
+
+#: ../../node/resources/events.rst:232
+msgid "The content of next subtitle"
+msgstr ""
+
+#: ../../node/resources/events.rst:236
+msgid "subtitleDone"
+msgstr ""
+
+#: ../../node/resources/events.rst:238
+msgid "Called when the subtitle is done."
+msgstr ""
+
+#: ../../node/resources/events.rst:249
+msgid "PLAYER_TRACEBACK"
+msgstr ""
+
+#: ../../node/resources/events.rst:251
+msgid ""
+"Called when the player gets some traceback while trying to send packets to "
+"discord server."
+msgstr ""
+
+#: ../../node/resources/events.rst:258 ../../node/resources/events.rst:273
+msgid "traceback"
+msgstr ""
+
+#: ../../node/resources/events.rst:258 ../../node/resources/events.rst:273
+msgid "The traceback information which the player gets"
+msgstr ""
+
+#: ../../node/resources/events.rst:262
+msgid "SOURCE_TRACEBACK"
+msgstr ""
+
+#: ../../node/resources/events.rst:264
+msgid ""
+"Called when the player gets some traceback while trying to play the source. "
+"That source will be automatically removed from the queue."
+msgstr ""
+
+#: ../../node/resources/events.rst:271
+msgid "Union[AudioData, AudioSource]"
+msgstr ""
+
+#: ../../node/resources/events.rst:271
+msgid "The source which the player gets traceback while trying to play"
+msgstr ""
diff --git a/docs/locale/ko/LC_MESSAGES/node/resources/source.po b/docs/locale/ko/LC_MESSAGES/node/resources/source.po
new file mode 100644
index 00000000..0c687443
--- /dev/null
+++ b/docs/locale/ko/LC_MESSAGES/node/resources/source.po
@@ -0,0 +1,350 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2021, kijk2869
+# This file is distributed under the same license as the discodo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: discodo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-03-05 11:56+0900\n"
+"PO-Revision-Date: 2021-03-05 02:57+0000\n"
+"Language-Team: Korean (https://www.transifex.com/mary-2/teams/117665/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../node/resources/source.rst:2
+msgid "AudioData"
+msgstr ""
+
+#: ../../node/resources/source.rst:5
+msgid "AudioData Object"
+msgstr ""
+
+#: ../../node/resources/source.rst:8
+msgid "AudioData Structure"
+msgstr ""
+
+#: ../../node/resources/source.rst:11 ../../node/resources/source.rst:53
+msgid "Field"
+msgstr ""
+
+#: ../../node/resources/source.rst:11 ../../node/resources/source.rst:53
+msgid "Type"
+msgstr ""
+
+#: ../../node/resources/source.rst:11 ../../node/resources/source.rst:53
+msgid "Description"
+msgstr ""
+
+#: ../../node/resources/source.rst:13 ../../node/resources/source.rst:55
+msgid "_type"
+msgstr ""
+
+#: ../../node/resources/source.rst:13 ../../node/resources/source.rst:15
+#: ../../node/resources/source.rst:17 ../../node/resources/source.rst:55
+#: ../../node/resources/source.rst:57 ../../node/resources/source.rst:59
+msgid "string"
+msgstr ""
+
+#: ../../node/resources/source.rst:13
+msgid "fixed value ``AudioData``"
+msgstr ""
+
+#: ../../node/resources/source.rst:15 ../../node/resources/source.rst:57
+msgid "tag"
+msgstr ""
+
+#: ../../node/resources/source.rst:15 ../../node/resources/source.rst:57
+msgid "object tag (uuid)"
+msgstr ""
+
+#: ../../node/resources/source.rst:17 ../../node/resources/source.rst:59
+msgid "id"
+msgstr ""
+
+#: ../../node/resources/source.rst:17 ../../node/resources/source.rst:59
+msgid "source id"
+msgstr ""
+
+#: ../../node/resources/source.rst:19 ../../node/resources/source.rst:61
+msgid "title"
+msgstr ""
+
+#: ../../node/resources/source.rst:19 ../../node/resources/source.rst:21
+#: ../../node/resources/source.rst:23 ../../node/resources/source.rst:25
+#: ../../node/resources/source.rst:31 ../../node/resources/source.rst:33
+#: ../../node/resources/source.rst:61 ../../node/resources/source.rst:63
+#: ../../node/resources/source.rst:65 ../../node/resources/source.rst:67
+#: ../../node/resources/source.rst:73 ../../node/resources/source.rst:75
+msgid "?string"
+msgstr ""
+
+#: ../../node/resources/source.rst:19 ../../node/resources/source.rst:61
+msgid "source title"
+msgstr ""
+
+#: ../../node/resources/source.rst:21 ../../node/resources/source.rst:63
+msgid "webpage_url"
+msgstr ""
+
+#: ../../node/resources/source.rst:21 ../../node/resources/source.rst:63
+msgid "source webpage url"
+msgstr ""
+
+#: ../../node/resources/source.rst:23 ../../node/resources/source.rst:65
+msgid "thumbnail"
+msgstr ""
+
+#: ../../node/resources/source.rst:23 ../../node/resources/source.rst:65
+msgid "source thumbnail"
+msgstr ""
+
+#: ../../node/resources/source.rst:25 ../../node/resources/source.rst:67
+msgid "url"
+msgstr ""
+
+#: ../../node/resources/source.rst:25 ../../node/resources/source.rst:67
+msgid "source stream url"
+msgstr ""
+
+#: ../../node/resources/source.rst:27 ../../node/resources/source.rst:69
+msgid "duration"
+msgstr ""
+
+#: ../../node/resources/source.rst:27
+msgid "?integer"
+msgstr ""
+
+#: ../../node/resources/source.rst:27 ../../node/resources/source.rst:69
+msgid "source duration"
+msgstr ""
+
+#: ../../node/resources/source.rst:29 ../../node/resources/source.rst:71
+msgid "is_live"
+msgstr ""
+
+#: ../../node/resources/source.rst:29 ../../node/resources/source.rst:39
+#: ../../node/resources/source.rst:71 ../../node/resources/source.rst:81
+#: ../../node/resources/source.rst:87
+msgid "boolean"
+msgstr ""
+
+#: ../../node/resources/source.rst:29 ../../node/resources/source.rst:71
+msgid "source live state"
+msgstr ""
+
+#: ../../node/resources/source.rst:31 ../../node/resources/source.rst:73
+msgid "uploader"
+msgstr ""
+
+#: ../../node/resources/source.rst:31 ../../node/resources/source.rst:73
+msgid "source uploader"
+msgstr ""
+
+#: ../../node/resources/source.rst:33 ../../node/resources/source.rst:75
+msgid "description"
+msgstr ""
+
+#: ../../node/resources/source.rst:33 ../../node/resources/source.rst:75
+msgid "source description"
+msgstr ""
+
+#: ../../node/resources/source.rst:35 ../../node/resources/source.rst:77
+msgid "subtitles"
+msgstr ""
+
+#: ../../node/resources/source.rst:35 ../../node/resources/source.rst:37
+#: ../../node/resources/source.rst:41 ../../node/resources/source.rst:77
+#: ../../node/resources/source.rst:79 ../../node/resources/source.rst:83
+msgid "json"
+msgstr ""
+
+#: ../../node/resources/source.rst:35 ../../node/resources/source.rst:77
+msgid "source subtitles (Mostly SRV1)"
+msgstr ""
+
+#: ../../node/resources/source.rst:37 ../../node/resources/source.rst:79
+msgid "chapters"
+msgstr ""
+
+#: ../../node/resources/source.rst:37 ../../node/resources/source.rst:79
+msgid "source chapters (Only in youtube)"
+msgstr ""
+
+#: ../../node/resources/source.rst:39 ../../node/resources/source.rst:81
+msgid "related"
+msgstr ""
+
+#: ../../node/resources/source.rst:39 ../../node/resources/source.rst:81
+msgid "related playing state"
+msgstr ""
+
+#: ../../node/resources/source.rst:41 ../../node/resources/source.rst:83
+msgid "context"
+msgstr ""
+
+#: ../../node/resources/source.rst:41 ../../node/resources/source.rst:83
+msgid "object context"
+msgstr ""
+
+#: ../../node/resources/source.rst:43 ../../node/resources/source.rst:85
+msgid "start_position"
+msgstr ""
+
+#: ../../node/resources/source.rst:43 ../../node/resources/source.rst:85
+msgid "float"
+msgstr ""
+
+#: ../../node/resources/source.rst:43
+msgid "source start position"
+msgstr ""
+
+#: ../../node/resources/source.rst:47
+msgid "AudioSource Object"
+msgstr ""
+
+#: ../../node/resources/source.rst:50
+msgid "AudioSource Structure"
+msgstr ""
+
+#: ../../node/resources/source.rst:55
+msgid "fixed value ``AudioSource``"
+msgstr ""
+
+#: ../../node/resources/source.rst:69
+msgid "integer"
+msgstr ""
+
+#: ../../node/resources/source.rst:85
+msgid "start position"
+msgstr ""
+
+#: ../../node/resources/source.rst:87
+msgid "seekable"
+msgstr ""
+
+#: ../../node/resources/source.rst:87
+msgid "seekable state"
+msgstr ""
+
+#: ../../node/resources/source.rst:89
+msgid "position"
+msgstr ""
+
+#: ../../node/resources/source.rst:89
+msgid "?float"
+msgstr ""
+
+#: ../../node/resources/source.rst:89
+msgid "source current position"
+msgstr ""
+
+#: ../../node/resources/source.rst:93
+msgid "Get From The Queue"
+msgstr ""
+
+#: ../../node/resources/source.rst:97
+msgid "The source object in queue"
+msgstr ""
+
+#: ../../node/resources/source.rst:99 ../../node/resources/source.rst:128
+#: ../../node/resources/source.rst:161
+msgid "**Example response**:"
+msgstr ""
+
+#: ../../node/resources/source.rst:0 ../../node/resources/source.rst:0
+#: ../../node/resources/source.rst:0
+msgid "Parameters"
+msgstr ""
+
+#: ../../node/resources/source.rst:107 ../../node/resources/source.rst:136
+#: ../../node/resources/source.rst:174
+msgid "the tag or index of source object"
+msgstr ""
+
+#: ../../node/resources/source.rst:0 ../../node/resources/source.rst:0
+#: ../../node/resources/source.rst:0
+msgid "Request Headers"
+msgstr ""
+
+#: ../../node/resources/source.rst:110 ../../node/resources/source.rst:139
+#: ../../node/resources/source.rst:177
+msgid "Password for discodo server"
+msgstr ""
+
+#: ../../node/resources/source.rst:112 ../../node/resources/source.rst:141
+#: ../../node/resources/source.rst:179
+msgid "the bot user id"
+msgstr ""
+
+#: ../../node/resources/source.rst:113 ../../node/resources/source.rst:142
+#: ../../node/resources/source.rst:180
+msgid "the bot shard id"
+msgstr ""
+
+#: ../../node/resources/source.rst:114 ../../node/resources/source.rst:143
+#: ../../node/resources/source.rst:181
+msgid "the guild id of queue"
+msgstr ""
+
+#: ../../node/resources/source.rst:115 ../../node/resources/source.rst:144
+#: ../../node/resources/source.rst:182
+msgid "the voiceclient id"
+msgstr ""
+
+#: ../../node/resources/source.rst:0 ../../node/resources/source.rst:0
+#: ../../node/resources/source.rst:0
+msgid "Status Codes"
+msgstr ""
+
+#: ../../node/resources/source.rst:117 ../../node/resources/source.rst:150
+#: ../../node/resources/source.rst:184
+msgid "no error"
+msgstr ""
+
+#: ../../node/resources/source.rst:118 ../../node/resources/source.rst:151
+#: ../../node/resources/source.rst:185
+msgid "authorization failed or VoiceClient-ID mismatched"
+msgstr ""
+
+#: ../../node/resources/source.rst:119 ../../node/resources/source.rst:152
+#: ../../node/resources/source.rst:186
+msgid "ClientManager or VoiceClient not found"
+msgstr ""
+
+#: ../../node/resources/source.rst:122
+msgid "Edit From The Queue"
+msgstr ""
+
+#: ../../node/resources/source.rst:126
+msgid "Edit the source object in queue"
+msgstr ""
+
+#: ../../node/resources/source.rst:0
+msgid "JSON Parameters"
+msgstr ""
+
+#: ../../node/resources/source.rst:146
+msgid "index to move the source in queue"
+msgstr ""
+
+#: ../../node/resources/source.rst:147
+msgid "context to save on the object"
+msgstr ""
+
+#: ../../node/resources/source.rst:148
+msgid "position to start on (only in AudioData)"
+msgstr ""
+
+#: ../../node/resources/source.rst:155
+msgid "Remove From The Queue"
+msgstr ""
+
+#: ../../node/resources/source.rst:159
+msgid "Remove the source object in queue"
+msgstr ""
diff --git a/docs/locale/ko/LC_MESSAGES/node/resources/voice_client.po b/docs/locale/ko/LC_MESSAGES/node/resources/voice_client.po
new file mode 100644
index 00000000..ce72b86f
--- /dev/null
+++ b/docs/locale/ko/LC_MESSAGES/node/resources/voice_client.po
@@ -0,0 +1,354 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2021, kijk2869
+# This file is distributed under the same license as the discodo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# Translators:
+# Mary <mareowst@gmail.com>, 2021
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: discodo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-03-07 18:34+0900\n"
+"PO-Revision-Date: 2021-03-05 03:56+0000\n"
+"Last-Translator: Mary <mareowst@gmail.com>, 2021\n"
+"Language-Team: Korean (https://www.transifex.com/mary-2/teams/117665/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../node/resources/voice_client.rst:2
+msgid "Voice Client"
+msgstr "음성 클라이언트"
+
+#: ../../node/resources/voice_client.rst:5
+msgid "Get State"
+msgstr "상태 가져오기"
+
+#: ../../node/resources/voice_client.rst:8
+msgid "> Example getState"
+msgstr "> getState 예제"
+
+#: ../../node/resources/voice_client.rst:21
+msgid "< Example getState response"
+msgstr "< getState 응답 예제"
+
+#: ../../node/resources/voice_client.rst:47
+msgid "Get Context"
+msgstr "컨텍스트 가져오기"
+
+#: ../../node/resources/voice_client.rst:51
+msgid "Get context of the voice client"
+msgstr "음성 클라이언트의 컨텍스트를 가져옵니다"
+
+#: ../../node/resources/voice_client.rst:53
+#: ../../node/resources/voice_client.rst:79
+#: ../../node/resources/voice_client.rst:107
+#: ../../node/resources/voice_client.rst:137
+#: ../../node/resources/voice_client.rst:167
+#: ../../node/resources/voice_client.rst:196
+#: ../../node/resources/voice_client.rst:230
+#: ../../node/resources/voice_client.rst:332
+#: ../../node/resources/voice_client.rst:360
+msgid "**Example response**:"
+msgstr "**응답 예제**:"
+
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+msgid "Request Headers"
+msgstr "요청 헤더"
+
+#: ../../node/resources/voice_client.rst:61
+#: ../../node/resources/voice_client.rst:87
+#: ../../node/resources/voice_client.rst:117
+#: ../../node/resources/voice_client.rst:147
+#: ../../node/resources/voice_client.rst:178
+#: ../../node/resources/voice_client.rst:207
+#: ../../node/resources/voice_client.rst:240
+#: ../../node/resources/voice_client.rst:258
+#: ../../node/resources/voice_client.rst:278
+#: ../../node/resources/voice_client.rst:296
+#: ../../node/resources/voice_client.rst:314
+#: ../../node/resources/voice_client.rst:342
+#: ../../node/resources/voice_client.rst:370
+msgid "Password for discodo server"
+msgstr "discodo 서버의 비밀번호"
+
+#: ../../node/resources/voice_client.rst:63
+#: ../../node/resources/voice_client.rst:89
+#: ../../node/resources/voice_client.rst:119
+#: ../../node/resources/voice_client.rst:149
+#: ../../node/resources/voice_client.rst:180
+#: ../../node/resources/voice_client.rst:209
+#: ../../node/resources/voice_client.rst:242
+#: ../../node/resources/voice_client.rst:260
+#: ../../node/resources/voice_client.rst:280
+#: ../../node/resources/voice_client.rst:298
+#: ../../node/resources/voice_client.rst:316
+#: ../../node/resources/voice_client.rst:344
+#: ../../node/resources/voice_client.rst:372
+msgid "the bot user id"
+msgstr "봇 아이디"
+
+#: ../../node/resources/voice_client.rst:64
+#: ../../node/resources/voice_client.rst:90
+#: ../../node/resources/voice_client.rst:120
+#: ../../node/resources/voice_client.rst:150
+#: ../../node/resources/voice_client.rst:181
+#: ../../node/resources/voice_client.rst:210
+#: ../../node/resources/voice_client.rst:243
+#: ../../node/resources/voice_client.rst:261
+#: ../../node/resources/voice_client.rst:281
+#: ../../node/resources/voice_client.rst:299
+#: ../../node/resources/voice_client.rst:317
+#: ../../node/resources/voice_client.rst:345
+#: ../../node/resources/voice_client.rst:373
+msgid "the bot shard id"
+msgstr "봇 샤드 아이디"
+
+#: ../../node/resources/voice_client.rst:65
+#: ../../node/resources/voice_client.rst:91
+#: ../../node/resources/voice_client.rst:121
+#: ../../node/resources/voice_client.rst:151
+#: ../../node/resources/voice_client.rst:182
+#: ../../node/resources/voice_client.rst:211
+#: ../../node/resources/voice_client.rst:244
+#: ../../node/resources/voice_client.rst:262
+#: ../../node/resources/voice_client.rst:282
+#: ../../node/resources/voice_client.rst:300
+#: ../../node/resources/voice_client.rst:318
+#: ../../node/resources/voice_client.rst:346
+#: ../../node/resources/voice_client.rst:374
+msgid "the guild id of queue"
+msgstr "재생목록의 길드 아이디"
+
+#: ../../node/resources/voice_client.rst:66
+#: ../../node/resources/voice_client.rst:92
+#: ../../node/resources/voice_client.rst:122
+#: ../../node/resources/voice_client.rst:152
+#: ../../node/resources/voice_client.rst:183
+#: ../../node/resources/voice_client.rst:212
+#: ../../node/resources/voice_client.rst:245
+#: ../../node/resources/voice_client.rst:263
+#: ../../node/resources/voice_client.rst:283
+#: ../../node/resources/voice_client.rst:301
+#: ../../node/resources/voice_client.rst:319
+#: ../../node/resources/voice_client.rst:347
+#: ../../node/resources/voice_client.rst:375
+msgid "the voiceclient id"
+msgstr "음성 클라이언트 아이디"
+
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+msgid "Status Codes"
+msgstr "상태 코드"
+
+#: ../../node/resources/voice_client.rst:68
+#: ../../node/resources/voice_client.rst:96
+#: ../../node/resources/voice_client.rst:126
+#: ../../node/resources/voice_client.rst:156
+#: ../../node/resources/voice_client.rst:185
+#: ../../node/resources/voice_client.rst:219
+#: ../../node/resources/voice_client.rst:247
+#: ../../node/resources/voice_client.rst:267
+#: ../../node/resources/voice_client.rst:285
+#: ../../node/resources/voice_client.rst:303
+#: ../../node/resources/voice_client.rst:321
+#: ../../node/resources/voice_client.rst:349
+#: ../../node/resources/voice_client.rst:377
+msgid "no error"
+msgstr "에러 없음"
+
+#: ../../node/resources/voice_client.rst:69
+#: ../../node/resources/voice_client.rst:97
+#: ../../node/resources/voice_client.rst:127
+#: ../../node/resources/voice_client.rst:157
+#: ../../node/resources/voice_client.rst:186
+#: ../../node/resources/voice_client.rst:220
+#: ../../node/resources/voice_client.rst:248
+#: ../../node/resources/voice_client.rst:268
+#: ../../node/resources/voice_client.rst:286
+#: ../../node/resources/voice_client.rst:304
+#: ../../node/resources/voice_client.rst:322
+#: ../../node/resources/voice_client.rst:350
+#: ../../node/resources/voice_client.rst:378
+msgid "authorization failed or VoiceClient-ID mismatched"
+msgstr "인증이 실패했거나 VoiceClient-ID가 일치하지 않음"
+
+#: ../../node/resources/voice_client.rst:70
+#: ../../node/resources/voice_client.rst:98
+#: ../../node/resources/voice_client.rst:128
+#: ../../node/resources/voice_client.rst:158
+#: ../../node/resources/voice_client.rst:187
+#: ../../node/resources/voice_client.rst:221
+#: ../../node/resources/voice_client.rst:249
+#: ../../node/resources/voice_client.rst:269
+#: ../../node/resources/voice_client.rst:287
+#: ../../node/resources/voice_client.rst:305
+#: ../../node/resources/voice_client.rst:323
+#: ../../node/resources/voice_client.rst:351
+#: ../../node/resources/voice_client.rst:379
+msgid "ClientManager or VoiceClient not found"
+msgstr "클라이언트 매니저 혹은 음성 클라이언트를 찾을 수 없음"
+
+#: ../../node/resources/voice_client.rst:73
+msgid "Set Context"
+msgstr "컨텍스트 설정하기"
+
+#: ../../node/resources/voice_client.rst:77
+msgid "Set context of the voice client"
+msgstr "음성 클라이언트의 컨텍스트를 설정합니다"
+
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+#: ../../node/resources/voice_client.rst:0
+msgid "JSON Parameters"
+msgstr "JSON 파라미터"
+
+#: ../../node/resources/voice_client.rst:94
+msgid "context to set"
+msgstr "설정할 컨텍스트"
+
+#: ../../node/resources/voice_client.rst:101
+msgid "Put Source"
+msgstr "소스 추가하기"
+
+#: ../../node/resources/voice_client.rst:105
+msgid "Put the source object on Queue"
+msgstr "재생목록에 소스 객체를 추가합니다"
+
+#: ../../node/resources/voice_client.rst:124
+msgid "the source object to put"
+msgstr "추가할 소스 객체"
+
+#: ../../node/resources/voice_client.rst:131
+msgid "Load Source"
+msgstr "소스 로드하기"
+
+#: ../../node/resources/voice_client.rst:135
+msgid "Search query and put it on Queue"
+msgstr "요청을 검색하고 재생목록에 추가합니다"
+
+#: ../../node/resources/voice_client.rst:154
+msgid "query to search"
+msgstr "검색할 요청"
+
+#: ../../node/resources/voice_client.rst:161
+msgid "Get Options"
+msgstr "옵션 가져오기"
+
+#: ../../node/resources/voice_client.rst:165
+msgid "Get options of the voice_client"
+msgstr "음성 클라이언트의 옵션을 가져옵니다"
+
+#: ../../node/resources/voice_client.rst:190
+msgid "Set Options"
+msgstr "옵션 설정하기"
+
+#: ../../node/resources/voice_client.rst:194
+msgid "Set options of the voice_client"
+msgstr "음성 클라이언트의 옵션을 설정합니다"
+
+#: ../../node/resources/voice_client.rst:214
+msgid "volume value"
+msgstr "볼륨 값"
+
+#: ../../node/resources/voice_client.rst:215
+msgid "crossfade value"
+msgstr "크로스페이드 값"
+
+#: ../../node/resources/voice_client.rst:216
+msgid "autoplay value"
+msgstr "자동재생 값"
+
+#: ../../node/resources/voice_client.rst:217
+msgid "filter value"
+msgstr "필터 값"
+
+#: ../../node/resources/voice_client.rst:224
+msgid "Get Position"
+msgstr "위치 가져오기"
+
+#: ../../node/resources/voice_client.rst:228
+msgid "Get position of the voice_client"
+msgstr "음성 클라이언트의 위치를 가져옵니다<br>"
+
+#: ../../node/resources/voice_client.rst:252
+msgid "Set Position (Seek)"
+msgstr "위치 설정하기 (탐색)"
+
+#: ../../node/resources/voice_client.rst:256
+msgid "Set position of the voice_client"
+msgstr "음성 클라이언트의 위치를 설정합니다"
+
+#: ../../node/resources/voice_client.rst:265
+msgid "position to seek"
+msgstr "탐색할 위치"
+
+#: ../../node/resources/voice_client.rst:272
+msgid "Skip Current"
+msgstr "플레이어 스킵"
+
+#: ../../node/resources/voice_client.rst:276
+msgid "Skip current of the voice_client"
+msgstr "현재 재생중인 소스를 스킵합니다"
+
+#: ../../node/resources/voice_client.rst:290
+msgid "Pause"
+msgstr "일시 정지"
+
+#: ../../node/resources/voice_client.rst:294
+msgid "Pause current of the voice_client"
+msgstr "현재 재생중인 소스를 일시 정지합니다"
+
+#: ../../node/resources/voice_client.rst:308
+msgid "Resume"
+msgstr "재개"
+
+#: ../../node/resources/voice_client.rst:312
+msgid "Resume current of the voice_client"
+msgstr "현재 일시 정지 중인 소스를 재개합니다"
+
+#: ../../node/resources/voice_client.rst:326
+msgid "Shuffle Queue"
+msgstr "재생 목록 섞기"
+
+#: ../../node/resources/voice_client.rst:330
+msgid "Shuffle the queue of the voice_client"
+msgstr "음성 클라이언트의 재생 목록을 섞습니다"
+
+#: ../../node/resources/voice_client.rst:354
+msgid "Get Queue"
+msgstr "재생 목록 가져오기"
+
+#: ../../node/resources/voice_client.rst:358
+msgid "Get the queue of the voice_client"
+msgstr "음성 클라이언트의 재생 목록을 가져옵니다"
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 00000000..922152e9
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.http://sphinx-doc.org/
+	exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/docs/node/connection.rst b/docs/node/connection.rst
new file mode 100644
index 00000000..3614c1e0
--- /dev/null
+++ b/docs/node/connection.rst
@@ -0,0 +1,128 @@
+Websocket Connection
+====================
+
+Discodo using websocket to send and receive events.
+
+Payloads
+--------
+
+Payload Structure
+~~~~~~~~~~~~~~~~~
+
+======= ===================== =================================
+ Field        Type                   Description
+------- --------------------- ---------------------------------
+ op            string          operation name for the payload
+------- --------------------- ---------------------------------
+ d       ?mixed (Mostly JSON)            event data
+======= ===================== =================================
+
+Connecting to Discodo
+---------------------
+
+Connecting
+~~~~~~~~~~
+
+Websocket Headers
+^^^^^^^^^^^^^^^^^
+
+============== ===================== =================================
+ Field            Type                   Description
+-------------- --------------------- ---------------------------------
+ Authorization            string          Password for discodo server
+============== ===================== =================================
+
+Once connected, the client should immediately receive ``HELLO`` with the connection's heartbeat interval unless you missed headers or mismatched, otherwise receive ``FORBIDDEN``.
+
+> Example HELLO
+^^^^^^^^^^^^^^^
+
+.. code-block:: json
+
+    {
+        "op": "HELLO",
+        "d": {
+            "heartbeat_interval": 15.0
+        }
+    }
+
+> Example FORBIDDEN
+^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: json
+
+    {
+        "op": "FORBIDDEN",
+        "d": "why the connection forbidden"
+    }
+
+Heartbeating
+~~~~~~~~~~~~
+
+Recieveing ``HELLO`` payload, the client should begin sending ``HEARTBEAT`` every ``heartbeat_interval`` seconds, until the connection closed.
+
+< Example HEARTBEAT
+^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: json5
+
+    {
+        "op": "HEARTBEAT",
+        "d": 0 // timestamp
+    }
+
+Event Data (``d``) can be ``None``, the server will echo them.
+
+> Example HEARTBEAT_ACK
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: json5
+
+    {
+        "op": "HEARTBEAT_ACK",
+        "d": 0 // timestamp
+    }
+
+Identifying
+~~~~~~~~~~~
+
+The client must send ``IDENTIFY`` to configure the audio manager before using.
+
+< Example IDENTIFY
+^^^^^^^^^^^^^^^^^^
+
+.. code-block:: json5
+
+    {
+        "op": "IDENTIFY",
+        "d": {
+            "user_id": "my bot id"
+            "shard_id": null // shard id
+        }
+    }
+
+Resumed
+~~~~~~~
+
+If the same ``user_id`` with the same ``shard_id`` is connected before ``VC_TIMEOUT``, it will be resumed.
+
+> Example Resumed
+^^^^^^^^^^^^^^^^^
+
+.. code-block:: json5
+
+    {
+        "op": "RESUMED",
+        "d": {
+            "voice_clients": [
+                [0, 0] // guild_id, voice_channel_id(can be null)
+            ]
+        }
+    }
+
+When the client recieve ``RESUMED``, must reconnect to each voice channel.
+
+Disconnecting
+~~~~~~~~~~~~~
+
+If the connection is closed, the server will clean up manager and sources after ``VC_TIMEOUT``
diff --git a/docs/node/resources/events.rst b/docs/node/resources/events.rst
new file mode 100644
index 00000000..cd583159
--- /dev/null
+++ b/docs/node/resources/events.rst
@@ -0,0 +1,274 @@
+.. _node_events:
+
+Event Reference
+===============
+
+This section outlines the different types of events dispatched by discodo node with websocket.
+
+.. note::
+    If you are using :py:class:`DPYClient`, the events that you get will have something different. See this :ref:`client_events`.
+
+STATUS
+------
+
+Called when the client requests system information by ``getStatus``. the unit is mega bytes or percent.
+
+================= ===================== ==================================
+ Field             Type                  Description
+----------------- --------------------- ----------------------------------
+ UsedMemory        integer               The process memory usage
+----------------- --------------------- ----------------------------------
+ TotalMemory       integer               The system memory usage
+----------------- --------------------- ----------------------------------
+ ProcessLoad       integer               The process cpu usage
+----------------- --------------------- ----------------------------------
+ TotalLoad         integer               The system cpu usage
+----------------- --------------------- ----------------------------------
+ Cores             integer               The cpu core count
+----------------- --------------------- ----------------------------------
+ Threads           integer               The process thread count
+----------------- --------------------- ----------------------------------
+ NetworkInbound    integer               The network inbound counters
+----------------- --------------------- ----------------------------------
+ NetworkOutbound   integer               The network outbound counters
+================= ===================== ==================================
+
+HEARTBEAT_ACK
+-------------
+
+Called when the client send ``HEARTBEAT`` payload. The data of this event is payload data.
+
+IDENTIFIED
+----------
+
+Called when the new voice client has successfully created. This is not the same as the client being fully connected.
+
+================ ===================== ==================================
+ Field            Type                  Description
+---------------- --------------------- ----------------------------------
+ guild_id         int                   The guild id of the voice client
+---------------- --------------------- ----------------------------------
+ id               str                   The id of the voice client
+================ ===================== ==================================
+
+VC_DESTROYED
+------------
+
+Called when the voice client has successfully destroyed.
+
+.. note::
+    This does not mean that the bot have disconnected from the voice channel. When the client receives this event, it should disconnect from the voice channel.
+
+================ ===================== ====================================================
+ Field            Type                  Description
+---------------- --------------------- ----------------------------------------------------
+ guild_id         int                   The guild id of the voice client that is destroyed
+================ ===================== ====================================================
+
+QUEUE_EVENT
+-----------
+
+Called when there is something changed in the queue of the voice client.
+
+================ ===================== ==================================
+ Field            Type                  Description
+---------------- --------------------- ----------------------------------
+ guild_id         int                   The guild id of the voice client
+---------------- --------------------- ----------------------------------
+ name             str                   The name of the method
+---------------- --------------------- ----------------------------------
+ args             list                  The arguments of the method
+================ ===================== ==================================
+
+VC_CHANNEL_EDITED
+-----------------
+
+Called when the voice channel of the voice client is changed.
+
+================ ===================== ===================================
+ Field            Type                  Description
+---------------- --------------------- -----------------------------------
+ guild_id         int                   The guild id of the voice client
+---------------- --------------------- -----------------------------------
+ channel_id       int                   The channel id of the voice client
+================ ===================== ===================================
+
+putSource
+---------
+
+Called when some sources are put in the queue.
+
+================ ===================== ===================================
+ Field            Type                  Description
+---------------- --------------------- -----------------------------------
+ guild_id         int                   The guild id of the voice client
+---------------- --------------------- -----------------------------------
+ sources          list                  The sources which is put
+================ ===================== ===================================
+
+loadSource
+----------
+
+Called when some sources are searched and put in the queue.
+
+================ ======================= =======================================
+ Field            Type                    Description
+---------------- ----------------------- ---------------------------------------
+ guild_id         int                     The guild id of the voice client
+---------------- ----------------------- ---------------------------------------
+ source           list or AudioData       The sources which is searched and put
+================ ======================= =======================================
+
+REQUIRE_NEXT_SOURCE
+-------------------
+
+Called when the player needs next source to play. If you set ``autoplay`` as ``True``, the related source will be put after this event.
+
+================ ======================= ==================================================
+ Field            Type                    Description
+---------------- ----------------------- --------------------------------------------------
+ guild_id         int                     The guild id of the voice client
+---------------- ----------------------- --------------------------------------------------
+ current          AudioSource             The source which the player is currently playing
+================ ======================= ==================================================
+
+SOURCE_START
+------------
+
+Called when the player starts to play the source.
+
+================ ======================= ============================================
+ Field            Type                    Description
+---------------- ----------------------- --------------------------------------------
+ guild_id         int                     The guild id of the voice client
+---------------- ----------------------- --------------------------------------------
+ source           AudioSource             The source which the player starts to play
+================ ======================= ============================================
+
+SOURCE_STOP
+-----------
+
+Called when the player stops to play the source.
+
+================ ======================= ============================================
+ Field            Type                    Description
+---------------- ----------------------- --------------------------------------------
+ guild_id         int                     The guild id of the voice client
+---------------- ----------------------- --------------------------------------------
+ source           AudioSource             The source which the player stops to play
+================ ======================= ============================================
+
+getState
+--------
+
+Called when the client requests the player state by ``getState``.
+
+================ =============================== ================================================
+ Field            Type                            Description
+---------------- ------------------------------- ------------------------------------------------
+ guild_id         str                             The guild id of the voice client
+---------------- ------------------------------- ------------------------------------------------
+ channel_id       str                             The channel id of the voice client
+---------------- ------------------------------- ------------------------------------------------
+ state            str                             Current state of the voice client
+---------------- ------------------------------- ------------------------------------------------
+ current          AudioSource                     Current source of the player
+---------------- ------------------------------- ------------------------------------------------
+ duration         float                           Current duration of the source that is playing
+---------------- ------------------------------- ------------------------------------------------
+ position         float                           Current position of the source that is playing
+---------------- ------------------------------- ------------------------------------------------
+ remain           float                           (duration value) - (position value)
+---------------- ------------------------------- ------------------------------------------------
+ remainQueue      int                             Current queue length of the player
+---------------- ------------------------------- ------------------------------------------------
+ options          JSON                            Current options of the player
+================ =============================== ================================================
+
+getQueue
+--------
+
+Called when the client requests the player queue by ``getQueue``.
+
+================ =============================== =================================
+ Field            Type                            Description
+---------------- ------------------------------- ---------------------------------
+ guild_id         int                             The guild id of the voice client
+---------------- ------------------------------- ---------------------------------
+ entries          list                            The entries of the queue
+================ =============================== =================================
+
+requestSubtitle
+---------------
+
+Called when the client requests synced subtitles by ``requestSubtitle``.
+
+================ =============================== =================================
+ Field            Type                            Description
+---------------- ------------------------------- ---------------------------------
+ guild_id         int                             The guild id of the voice client
+---------------- ------------------------------- ---------------------------------
+ identify         str                             The id to identify the subtitle
+---------------- ------------------------------- ---------------------------------
+ url              str                             The url of the subtitle
+================ =============================== =================================
+
+Subtitle
+--------
+
+This event is for sending the sync subtitle. This event is sent according to the player's position.
+
+================ =============================== =================================
+ Field            Type                            Description
+---------------- ------------------------------- ---------------------------------
+ guild_id         int                             The guild id of the voice client
+---------------- ------------------------------- ---------------------------------
+ identify         str                             The id to identify the subtitle
+---------------- ------------------------------- ---------------------------------
+ previous         str                             The content of previous subtitle
+---------------- ------------------------------- ---------------------------------
+ current          str                             The content of current subtitle
+---------------- ------------------------------- ---------------------------------
+ next             str                             The content of next subtitle
+================ =============================== =================================
+
+subtitleDone
+------------
+
+Called when the subtitle is done.
+
+================ =============================== =================================
+ Field            Type                            Description
+---------------- ------------------------------- ---------------------------------
+ guild_id         int                             The guild id of the voice client
+---------------- ------------------------------- ---------------------------------
+ identify         str                             The id to identify the subtitle
+================ =============================== =================================
+
+PLAYER_TRACEBACK
+----------------
+
+Called when the player gets some traceback while trying to send packets to discord server.
+
+================ =============================== ================================================================
+ Field            Type                            Description
+---------------- ------------------------------- ----------------------------------------------------------------
+ guild_id         int                             The guild id of the voice client
+---------------- ------------------------------- ----------------------------------------------------------------
+ traceback        str                             The traceback information which the player gets
+================ =============================== ================================================================
+
+SOURCE_TRACEBACK
+----------------
+
+Called when the player gets some traceback while trying to play the source. That source will be automatically removed from the queue.
+
+================ =============================== ================================================================
+ Field            Type                            Description
+---------------- ------------------------------- ----------------------------------------------------------------
+ guild_id         int                             The guild id of the voice client
+---------------- ------------------------------- ----------------------------------------------------------------
+ source           Union[AudioData, AudioSource]   The source which the player gets traceback while trying to play
+---------------- ------------------------------- ----------------------------------------------------------------
+ traceback        str                             The traceback information which the player gets
+================ =============================== ================================================================
diff --git a/docs/node/resources/source.rst b/docs/node/resources/source.rst
new file mode 100644
index 00000000..858574ef
--- /dev/null
+++ b/docs/node/resources/source.rst
@@ -0,0 +1,240 @@
+AudioData
+=========
+
+AudioData Object
+----------------
+
+AudioData Structure
+~~~~~~~~~~~~~~~~~~~
+
+================ ===================== ==================================
+ Field            Type                  Description
+---------------- --------------------- ----------------------------------
+ _type            string                fixed value ``AudioData``
+---------------- --------------------- ----------------------------------
+ tag              string                object tag (uuid)
+---------------- --------------------- ----------------------------------
+ id               string                source id
+---------------- --------------------- ----------------------------------
+ title            ?string               source title
+---------------- --------------------- ----------------------------------
+ webpage_url      ?string               source webpage url
+---------------- --------------------- ----------------------------------
+ thumbnail        ?string               source thumbnail
+---------------- --------------------- ----------------------------------
+ url              ?string               source stream url
+---------------- --------------------- ----------------------------------
+ duration         ?integer              source duration
+---------------- --------------------- ----------------------------------
+ is_live          boolean               source live state
+---------------- --------------------- ----------------------------------
+ uploader         ?string               source uploader
+---------------- --------------------- ----------------------------------
+ description      ?string               source description
+---------------- --------------------- ----------------------------------
+ subtitles        json                  source subtitles (Mostly SRV1)
+---------------- --------------------- ----------------------------------
+ chapters         json                  source chapters (Only in youtube)
+---------------- --------------------- ----------------------------------
+ related          boolean               related playing state
+---------------- --------------------- ----------------------------------
+ context          json                  object context
+---------------- --------------------- ----------------------------------
+ start_position   float                 source start position
+================ ===================== ==================================
+
+AudioSource Object
+------------------
+
+AudioSource Structure
+~~~~~~~~~~~~~~~~~~~~~
+
+================ ===================== ==================================
+ Field            Type                  Description
+---------------- --------------------- ----------------------------------
+ _type            string                fixed value ``AudioSource``
+---------------- --------------------- ----------------------------------
+ tag              string                object tag (uuid)
+---------------- --------------------- ----------------------------------
+ id               string                source id
+---------------- --------------------- ----------------------------------
+ title            ?string               source title
+---------------- --------------------- ----------------------------------
+ webpage_url      ?string               source webpage url
+---------------- --------------------- ----------------------------------
+ thumbnail        ?string               source thumbnail
+---------------- --------------------- ----------------------------------
+ url              ?string               source stream url
+---------------- --------------------- ----------------------------------
+ duration         integer              source duration
+---------------- --------------------- ----------------------------------
+ is_live          boolean               source live state
+---------------- --------------------- ----------------------------------
+ uploader         ?string               source uploader
+---------------- --------------------- ----------------------------------
+ description      ?string               source description
+---------------- --------------------- ----------------------------------
+ subtitles        json                  source subtitles (Mostly SRV1)
+---------------- --------------------- ----------------------------------
+ chapters         json                  source chapters (Only in youtube)
+---------------- --------------------- ----------------------------------
+ related          boolean               related playing state
+---------------- --------------------- ----------------------------------
+ context          json                  object context
+---------------- --------------------- ----------------------------------
+ start_position   float                 start position
+---------------- --------------------- ----------------------------------
+ seekable         boolean               seekable state
+---------------- --------------------- ----------------------------------
+ position         ?float                source current position
+================ ===================== ==================================
+
+Get Current
+-----------
+
+.. http:get:: /current
+
+    The source object that is currently playing
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            // source object
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Get From The Queue
+------------------
+
+.. http:get:: /queue/{tag_or_index}
+
+    The source object in queue
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            // source object
+        }
+
+    :param tag_or_index: the tag or index of source object
+    :type tag_or_index: int or str
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Edit Current
+------------
+
+.. http:post:: /current
+
+    Edit the source object that is currently playing
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            // source object
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :jsonparam json ?context: context to save on the object
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Edit From The Queue
+-------------------
+
+.. http:post:: /queue/{tag_or_index}
+
+    Edit the source object in queue
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            // edited source object
+        }
+
+    :param tag_or_index: the tag or index of source object
+    :type tag_or_index: int or str
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :jsonparam integer ?index: index to move the source in queue
+    :jsonparam json ?context: context to save on the object
+    :jsonparam float ?start_position: position to start on (only in AudioData)
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Remove From The Queue
+---------------------
+
+.. http:delete:: /queue/{tag_or_index}
+
+    Remove the source object in queue
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            "removed": {
+                // removed source object
+            },
+            "entries": [
+                // list of source in queue
+            ]
+        }
+
+    :param tag_or_index: the tag or index of source object
+    :type tag_or_index: int or str
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
diff --git a/docs/node/resources/voice_client.rst b/docs/node/resources/voice_client.rst
new file mode 100644
index 00000000..740ced50
--- /dev/null
+++ b/docs/node/resources/voice_client.rst
@@ -0,0 +1,380 @@
+Voice Client
+============
+
+Get State
+---------
+
+> Example getState
+^^^^^^^^^^^^^^^^^^
+
+.. code-block:: json
+
+    {
+        "op": "getState",
+        "d": {
+            "guild_id": "guild_id"
+        }
+    }
+
+
+< Example getState response
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: json
+
+    {
+        "op": "getState",
+        "d": {
+            "id": "VoiceClient ID",
+            "guild_id": "Guild ID",
+            "channel_id": "Voice Channel ID",
+            "state": "Current Player State",
+            "current": "Current AudioSource Object",
+            "duration": "Current source duration",
+            "position": "Current source position",
+            "remain": "Current source remain",
+            "remainQueue": "Queue length",
+            "options": {
+                "autoplay": "autoplay boolean",
+                "volume": "volume float",
+                "crossfade": "crossfade float",
+                "filter": {}
+            },
+            "context": {}
+        }
+    }
+
+Get Context
+-----------
+
+.. http:get:: /context
+
+    Get context of the voice client
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            // context
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Set Context
+-----------
+
+.. http:post:: /context
+
+    Set context of the voice client
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            // context
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :jsonparam json context: context to set
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Put Source
+----------
+
+.. http:post:: /putSource
+
+    Put the source object on Queue
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            "source": {
+                // source object
+            }
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :jsonparam json source: the source object to put
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Load Source
+-----------
+
+.. http:post:: /loadSource
+
+    Search query and put it on Queue
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            "source": {
+                // source object
+            }
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :jsonparam string query: query to search
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Get Options
+-----------
+
+.. http:get:: /options
+
+    Get options of the voice_client
+
+    **Example response**:
+
+    .. code-block:: json
+
+        {
+            "autoplay": True,
+            "volume": 1.0,
+            "crossfade": 10.0,
+            "filter": {}
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Set Options
+-----------
+
+.. http:post:: /options
+
+    Set options of the voice_client
+
+    **Example response**:
+
+    .. code-block:: json
+
+        {
+            "autoplay": True,
+            "volume": 1.0,
+            "crossfade": 10.0,
+            "filter": {}
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :jsonparam float ?volume: volume value
+    :jsonparam float ?crossafde: crossfade value
+    :jsonparam boolean ?autoplay: autoplay value
+    :jsonparam json ?filter: filter value
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Get Position
+------------
+
+.. http:get:: /seek
+
+    Get position of the voice_client
+
+    **Example response**:
+
+    .. code-block:: json
+
+        {
+            "duration": 300.0,
+            "position": 200.0,
+            "remain": 100.0
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Set Position (Seek)
+-------------------
+
+.. http:post:: /seek
+
+    Set position of the voice_client
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :jsonparam float offset: position to seek
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Skip Current
+------------
+
+.. http:post:: /skip
+
+    Skip current of the voice_client
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Pause
+-----
+
+.. http:post:: /pause
+
+    Pause current of the voice_client
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Resume
+------
+
+.. http:post:: /resume
+
+    Resume current of the voice_client
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Shuffle Queue
+-------------
+
+.. http:post:: /shuffle
+
+    Shuffle the queue of the voice_client
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            "entries": [
+                // source object
+            ]
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
+
+Get Queue
+---------
+
+.. http:get:: /queue
+
+    Get the queue of the voice_client
+
+    **Example response**:
+
+    .. code-block:: json5
+
+        {
+            "entries": [
+                // source object
+            ]
+        }
+
+    :reqheader Authorization: Password for discodo server
+
+    :reqheader User-ID: the bot user id
+    :reqheader ?Shard-ID: the bot shard id
+    :reqheader Guild-ID: the guild id of queue
+    :reqheader VoiceClient-ID: the voiceclient id
+
+    :statuscode 200: no error
+    :statuscode 403: authorization failed or VoiceClient-ID mismatched
+    :statuscode 404: ClientManager or VoiceClient not found
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 00000000..e4cc1b22
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,1 @@
+-r ../requirements-docs.txt
diff --git a/docs/server/README.md b/docs/server/README.md
deleted file mode 100644
index 2ea2f24e..00000000
--- a/docs/server/README.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Discodo Server Documentation
-
-## BaseUrls
-`--port PORT, -P PORT the port of the server (default: 8000)`
-
-Restful
-```
- http://<host>:<port>/
-```
-Websocket
-```
- ws://<host>:<port>/ws
-```
-
-## Contents
-
-### Introduction
-- [Setup](./setup.md)
-- [Connection](./connection.md)
-
-### Websocket
-- [Payloads](./payloads.md)
-- [Events](./events.md)
-
-### Restful
-- [Restful API](./restful.md)
diff --git a/docs/server/connection.md b/docs/server/connection.md
deleted file mode 100644
index b63aab64..00000000
--- a/docs/server/connection.md
+++ /dev/null
@@ -1,97 +0,0 @@
-# Websocket Connection
-
-## Connecting to Discodo
-
-### Connecting
-
-#### Websockets Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-
-If you missed headers or mismatched, the client recieves FORBIDDEN payload.
-
-#### Example FORBIDDEN
-
-```json
-{
-    "op": "FORBIDDEN",
-    "d": "why the connection forbidden"
-}
-```
-
-When connected, the client recieves HELLO payload with the connection's heartbeat interval.
-
-#### Example HELLO
-
-```json
-{
-    "op": "HELLO",
-    "d": {
-        "heartbeat_interval": 15.0
-    }
-}
-```
-
-### Heartbeating
-
-Recieveing HELLO payload, the client should begin sending HEARTBEAT payloads every `heartbeat_interval` seconds, until the connection closed.
-
-#### Exmaple HEARTBEAT Payload
-
-```json5
-{
-    "op": "HEARTBEAT",
-    "d": 1596417937 // timestamp
-}
-```
-
-Event Data (`d`) can be None, Server will echo them.
-
-#### Example HEARTBEAT_ACK
-
-```json5
-{
-    "op": "HEARTBEAT_ACK",
-    "d": 1596417937 // timestamp
-}
-```
-
-### Identifying
-
-The client must send IDENTIFY payload to configure the audio manager.
-
-#### Example IDENTIFY Payload
-
-```json
-{
-    "op": "IDENTIFY",
-    "d": {
-        "user_id": "my bot id"
-    }
-}
-```
-
-### Resumed
-
-If the same user id is connected before VC_TIMEOUT, it will be resumed.
-
-#### Example RESUMED Payload
-
-```json5
-{
-    "op": "RESUMED",
-    "d": {
-        "voice_clients": [
-            [0, 0] // guild_id, voicechannel_id(can be null)
-        ]
-    }
-}
-```
-
-If the client recieve RESUMED payload, must reconnect to the voice channel.
-
-### Disconnections
-
-If the connection is closed, Server will clean up manager and sources.
diff --git a/docs/server/events.md b/docs/server/events.md
deleted file mode 100644
index 0a4f7936..00000000
--- a/docs/server/events.md
+++ /dev/null
@@ -1,339 +0,0 @@
-# Websocket Events
-
-### HEARTBEAT_ACK
-
-```json5
-{
-    "op": "HEARTBEAT_ACK",
-    "d": 1596417937 // timestamp
-}
-```
-
-### STAT
-
-```json
-{
-    "UsedMemory": 0,
-    "TotalMemory": 0,
-    "ProcessLoad": 0,
-    "TotalLoad": 0,
-    "Cores": 0,
-    "Threads": 0,
-    "NetworkInbound": 0,
-    "NetworkOutbound": 0,
-    "TotalPlayer": 0
-}
-```
-
-### IDENTIFIED
-
-```json
-{
-    "op": "IDENTIFIED",
-    "d": "ClientManager initialized."
-}
-```
-
-## Audio Player
-
-May send after connect to voice channel.
-
-### VC_CREATED
-
-```json
-{
-    "op": "VC_CREATED",
-    "d": {
-        "guild_id": "guild id of audio player" 
-    }
-}
-```
-
-### VC_DESTROYED
-
-```json
-{
-    "op": "VC_DESTROYED",
-    "d": {
-        "guild_id": "guild id of audio player" 
-    }
-}
-```
-
-### setVolume
-
-```json5
-{
-    "op": "setVolume",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "volume": 1.0 // 0.0 ~ 2.0
-    }
-}
-```
-
-### setCrossfade
-
-```json
-{
-    "op": "setCrossfade",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "crossfade": 10.0
-    }
-}
-```
-
-### setAutoplay
-
-```json
-{
-    "op": "setAutoplay",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "autoplay": true
-    }
-}
-```
-
-### setGapless
-
-```json
-{
-    "op": "setGapless",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "gapless": true
-    }
-}
-```
-
-### setFilter
-
-```json5
-{
-    "op": "setFilter",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "filter": {
-            // libavfilter object
-        }
-    }
-}
-```
-### getSource
-
-```json5
-{
-    "op": "getSource",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "source": {
-            // Source object
-        }
-    }
-}
-```
-
-### loadSource
-
-```json5
-{
-    "op": "loadSource",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "source": {
-            // Source object
-        }
-    }
-}
-```
-
-### putSource
-
-```json5
-{
-    "op": "putSong",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "source": {
-            // Source object
-        }
-    }
-}
-```
-
-### REQUIRE_NEXT_SOURCE
-
-**If `autoplay` is on, related song will be added after this event.**
-
-```json5
-{
-    "op": "REQUIRE_NEXT_SOURCE",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "current": {
-            // Sourcec object
-        }
-    }
-}
-```
-
-### skip
-
-```json
-{
-    "op": "skip",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "remain": 0
-    }
-}
-```
-
-### seek
-
-```json
-{
-    "op": "seek",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "offset": 0
-    }
-}
-```
-
-### Queue
-
-```json5
-{
-    "op": "Queue",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "entries": [
-            // source objects
-        ]
-    }
-}
-```
-
-### State
-
-```json5
-{
-    "op": "getState",
-    "d": {
-        "id": "...",
-        "guild_id": "...",
-        "channel_id": "...",
-        "state": "...",
-        "current": {},
-        "duration": 0.0,
-        "position": 0.0,
-        "remain": 0.0,
-        "options": {
-            "autoplay": true,
-            "volume": 1.0,
-            "crossfade": 10.0,
-            "gapless": false,
-            "filter": {}
-        }
-    }
-}
-```
-
-### pause
-
-```json
-{
-    "op": "pause",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "state": "paused"
-    }
-}
-```
-
-### resume
-
-```json
-{
-    "op": "pause",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "state": "playing"
-    }
-}
-```
-
-### shuffle
-
-```json5
-{
-    "op": "shuffle",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "entries": [
-            // Source objects
-        ]
-    }
-}
-```
-
-### remove
-
-```json5
-{
-    "op": "remove",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "removed": {
-            // Source object
-        },
-        "entries": [
-            // Source objects
-        ]
-    }
-}
-```
-
-### requestSubtitle
-
-```json
-{
-    "op": "requestSubtitle",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "identify": "58b8b68f-e938-4699-b4ad-ffa262bf903d",
-        "url": "..."
-    }
-}
-```
-
-### Subtitle
-
-```json
-{
-    "op": "Subtitle",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "identify": "58b8b68f-e938-4699-b4ad-ffa262bf903d",
-        "previous": "Previous Lyrics",
-        "current": "Current Lyrics",
-        "next": "Next Lyrics"
-    }
-}
-```
-
-### subtitleDone
-
-```json
-{
-    "op": "subtitleDone",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "identify": "58b8b68f-e938-4699-b4ad-ffa262bf903d",
-        "url": "..."
-    }
-}
-```
\ No newline at end of file
diff --git a/docs/server/payloads.md b/docs/server/payloads.md
deleted file mode 100644
index 6250df23..00000000
--- a/docs/server/payloads.md
+++ /dev/null
@@ -1,296 +0,0 @@
-# Websocket Payloads
-
-#### Payload Structure
-
-|Key|Type|Description|
-|---|----|-----------|
-|op|string|the event name for this payload|
-|d|mixed (Mostly JSON)|the event data for this payload|
-
-### Sending Payloads
-
-#### Example Payload Dispatch
-
-```json
-{
-    "op": "the event name",
-    "d": {}
-}
-```
-
-### Exception while execute payload
-
-#### Example Payload Exception
-
-```json
-{
-    "op": "the event name",
-    "d": {
-        "traceback": "Python Traceback"
-    }
-}
-```
-
-### HEARTBEAT
-
-```json5
-{
-    "op": "HEARTBEAT",
-    "d": 1596417937 // timestamp
-}
-```
-
-### GET_STAT
-
-```json5
-{
-    "op": "GET_STAT"
-}
-```
-
-### IDENTIFY
-
-```json
-{
-    "op": "IDENTIFY",
-    "d": {
-        "user_id": "my bot id",
-        "session_id": "my bot session id to discord"
-    }
-}
-```
-
-### DISCORD_EVENT
-
-```json5
-{
-    "op": "DISCORD_EVENT",
-    "d": {
-        // Discord Socket Event Payloads
-    }
-}
-```
-**Just need `READY`, `RESUME`, `VOICE_STATE_UPDATE`, `VOICE_SERVER_UPDATE` payloads**
-
-## Audio Player
-
-Can use after connect to voice channel.
-
-### setVolume
-
-```json5
-{
-    "op": "setVolume",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "volume": 1 // 0.0~2.0
-    }
-}
-```
-
-### setCrossfade
-
-```json5
-{
-    "op": "setCrossfade",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "crossfade": 10 // seconds
-    }
-}
-```
-
-### setAutoplay
-
-```json5
-{
-    "op": "setAutoplay",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "autoplay": true
-    }
-}
-```
-
-### setGapless
-
-```json5
-{
-    "op": "setGapless",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "gapless": true // Can;t use with crossfade
-    }
-}
-```
-
-### setFilter
-
-```json5
-{
-    "op": "setFilter",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "filter": {
-            // libavfilter object
-        }
-    }
-}
-```
-
-### getSource
-
-```json
-{
-    "op": "getSource",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "query": "something to search"
-    }
-}
-```
-
-### loadSource
-
-```json
-{
-    "op": "loadSource",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "query": "something to search"
-    }
-}
-```
-
-### putSong
-
-```json5
-{
-    "op": "putSource",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "source": {
-            // Source Object
-        }
-    }
-}
-```
-
-### skip
-
-```json5
-{
-    "op": "skip",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "offset": 1 // number to skip song.
-    }
-}
-```
-
-### seek
-
-```json5
-{
-    "op": "seek",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "offset": 10 // seconds
-    }
-}
-```
-
-### getQueue
-
-```json
-{
-    "op": "getQueue",
-    "d": {
-        "guild_id": "guild id of audio player"
-    }
-}
-```
-
-### getState
-
-```json
-{
-    "op": "getState",
-    "d": {
-        "guild_id": "guild id of audio player"
-    }
-}
-```
-
-### pause
-
-```json
-{
-    "op": "pause",
-    "d": {
-        "guild_id": "guild id of audio player"
-    }
-}
-```
-
-### resume
-
-```json
-{
-    "op": "resume",
-    "d": {
-        "guild_id": "guild id of audio player"
-    }
-}
-```
-
-### shuffle
-
-```json
-{
-    "op": "shuffle",
-    "d": {
-        "guild_id": "guild id of audio player"
-    }
-}
-```
-
-### remove
-
-```json5
-{
-    "op": "remove",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "index": 1 // song number to remove
-    }
-}
-```
-
-### requestSubtitle
-
-Either `lang` or `url` is requested.
-
-Discodo supports `smi` and `srv1` type.
-
-```json5
-{
-    "op": "requestSubtitle",
-    "d": {
-        "guild_id": "guild id of audio player",
-        "lang": "en",
-        "url": "..."
-    }
-}
-```
-
-### VC_DESTROY
-
-```json
-{
-    "op": "VC_DESTROY",
-    "d": {
-        "guild_id": "guild id of audio player"
-    }
-}
-```
\ No newline at end of file
diff --git a/docs/server/restful.md b/docs/server/restful.md
deleted file mode 100644
index bc47664c..00000000
--- a/docs/server/restful.md
+++ /dev/null
@@ -1,508 +0,0 @@
-# Restful API
-
-## Routes
-
-### GET /status
-
-Check the resource status of the server.
-
-If you want to check size of players, use `GET_STAT` websocket payload.
-
-#### Response
-
-```json
-{
-    "UsedMemory": 0,
-    "TotalMemory": 0,
-    "ProcessLoad": 0,
-    "TotalLoad": 0,
-    "Cores": 0,
-    "Threads": 0,
-    "NetworkInbound": 0,
-    "NetworkOutbound": 0
-}
-```
-
-#### Response
-
-```json
-{
-    "_type": "AudioData",
-    "id": "...",
-    "title": "...",
-    "webpage_url": "...",
-    "thumbnail": "...",
-    "url": "...",
-    "duration": 0,
-    "is_live": true,
-    "uploader": "...",
-    "description": "...",
-}
-```
-
-### GET /planner
-
-Get the information of the Route Planner
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-
-#### Response
-
-```json
-{
-    "ipBlocks": [
-        {
-            "version": 4,
-            "broadcast_address": "0.0.0.0",
-            "size": 1
-        }
-    ],
-    "failedAddresses": [
-        {
-            "address": "0.0.0.0",
-            "status": 429,
-            "failed_at": 0
-        }
-    ]
-}
-```
-
-### POST /planner/unmark
-
-Unmark an address on planner's failed list.
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-
-#### Parameters
-
-|Key|Type|Description|
-|---|----|-----------|
-|address|string|address to unmark|
-
-#### Response
-
-```json
-{"status": 200}
-```
-
-### POST /planner/unmark/all
-
-Unmark all address on planner's failed list.
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-
-#### Response
-
-```json
-{"status": 200}
-```
-
-### GET /getSource
-
-Get the information of the video searched on Youtube
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-
-#### Parameters
-
-|Key|Type|Description|
-|---|----|-----------|
-|query|string|query to search on youtube|
-
-#### Response
-
-```json5
-{
-    "source": {
-        // Source Object
-    }
-}
-```
-
-### POST /putSource
-
-Put source object to queue
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json5
-{
-    "source": {
-        // Source Object
-    }
-}
-```
-
-#### Response
-
-```json
-{
-    "index": 0
-}
-```
-
-### POST /loadSource
-
-Load query from youtube and put source object to queue
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "query": "Something"
-}
-```
-
-#### Response
-
-```json5
-{
-    "source": {
-        // Source Object
-    }
-}
-```
-
-### POST /setVolume
-
-Set volume of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json5
-{
-    "volume": 1.0 // 0.0 ~ 2.0
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /setCrossfade
-
-Set crossfade of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "crossfade": 10.0
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /setGapless
-
-Set gapless of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "gapless": false
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /setAutoplay
-
-Set autoplay of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "autoplay": true
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /setFilter
-
-Set filter of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json5
-{
-    "filter": {
-        // libavfilter object
-    }
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /seek
-
-Seek source to offset of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "offset": 10.0
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /skip
-
-Skip source of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "offset": 1
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /pause
-
-Pause source of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-Empty `204`
-
-### POST /resume
-
-Resume source of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-Empty `204`
-
-### POST /shuffle
-
-Shuffle the queue of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-```json5
-{
-    "entries": [
-        {
-            // Source Object
-        }
-    ]
-}
-```
-
-### POST /remove
-
-Remove source object from the queue of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-```json5
-{
-    "removed": 1,
-    "entries": [
-        {
-            // Source Object
-        }
-    ]
-}
-```
-
-### GET /state
-
-Get state of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-```json
-{
-    "op": "getState",
-    "d": {
-        "id": "...",
-        "guild_id": "...",
-        "channel_id": "...",
-        "state": "...",
-        "current": {},
-        "duration": 0.0,
-        "position": 0.0,
-        "remain": 0.0,
-        "options": {
-            "autoplay": true,
-            "volume": 1.0,
-            "crossfade": 10.0,
-            "gapless": false,
-            "filter": {}
-        }
-    }
-}
-```
-
-### GET /queue
-
-Get state of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-```json5
-{
-    "entries": [
-        {
-            // Source Object
-        }
-    ]
-}
-```
diff --git a/docs/server/setup.md b/docs/server/setup.md
deleted file mode 100644
index e6275c60..00000000
--- a/docs/server/setup.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Introduction
-
-## Requirement
-
-Discodo works with **Python 3.7 or higher**.
-
-Ealier versions of Python may not be worked.
-
-## Installing
-
-You can get this library from [**PyPI**](https://pypi.org/project/discodo/):
-
-```sh
-python3 -m pip install --upgrade discodo
-```
-
-On Linux environments, more dependencies are required:
-
-- python3-dev
-- libopus-dev
-- libnacl-dev
-
-## Execution
-
-You can see additional options with the `--help` flag.
-
-```sh
-python3 -m discodo [-h] [--config CONFIG] [--host HOST] [--port PORT] [--auth AUTH] [--ws-interval WS_INTERVAL]
-               [--ws-timeout WS_TIMEOUT] [--ip IP] [--exclude-ip EXCLUDE_IP] [--default-volume DEFAULT_VOLUME]
-               [--default-crossfade DEFAULT_CROSSFADE] [--default-gapless DEFAULT_GAPLESS]
-               [--default-autoplay DEFAULT_AUTOPLAY] [--bufferlimit BUFFERLIMIT] [--preload PRELOAD]
-               [--timeout TIMEOUT] [--verbose]
-```
-
-## Options
-
-```
-optional arguments:
-  -h, --help            show this help message and exit
-  --version             show this library's version and exit
-  --config CONFIG       Config json file path (default: None)
-
-Webserver Option:
-  --host HOST, -H HOST  the hostname to listen on (default: 0.0.0.0)
-  --port PORT, -P PORT  the port of the webserver (default: 8000)
-  --auth AUTH, -A AUTH  the password of the webserver (default: hellodiscodo)
-  --ws-interval WS_INTERVAL
-                        heartbeat interval between discodo server and client (default: 15)
-  --ws-timeout WS_TIMEOUT
-                        seconds to close connection there is no respond from client (default: 60)
-
-Network Option:
-  --ip IP               Client-side IP addresses to use
-  --exclude-ip EXCLUDE_IP
-                        Client-side IP addresses not to use
-
-Player Option:
-  --default-volume DEFAULT_VOLUME
-                        player's default volume (default: 100)
-  --default-crossfade DEFAULT_CROSSFADE
-                        player's default crossfade seconds (default: 10.0)
-  --default-gapless DEFAULT_GAPLESS
-                        player's default gapless state (default: False)
-  --default-autoplay DEFAULT_AUTOPLAY
-                        player's default auto related play state (default: True)
-  --bufferlimit BUFFERLIMIT
-                        seconds of audio will be loaded in buffer (default: 5)
-  --preload PRELOAD     seconds to load next song before this song ends (default: 10)
-  --timeout TIMEOUT     seconds to cleanup player when connection of discord terminated (default: 300)
-
-Logging Option:
-  --verbose, -v         Print various debugging information
-```
-
-### Config file
-
-Must pass `json` file to `--config` flag.
-
-```json
-{
-    "HOST": "0.0.0.0",
-    "PORT": 8000,
-    "PASSWORD": "hellodiscodo",
-    "HANDSHAKE_INTERVAL": 15,
-    "HANDSHAKE_TIMEOUT": 60,
-    "IPBLOCKS": [],
-    "EXCLUDEIPS": [],
-    "DEFAULT_AUTOPLAY": true,
-    "DEFAULT_VOLUME": 1,
-    "DEFAULT_CROSSFADE": 10,
-    "DEFAULT_GAPLESS": false,
-    "BUFFERLIMIT": 5,
-    "PRELOAD_TIME": 10,
-    "VCTIMEOUT": 300,
-    "ENABLED_EXT_RESOLVER": [
-        "melon",
-        "spotify"
-    ],
-    "SPOTIFY_ID": null,
-    "SPOTIFY_SECRET": null
-}
-```
\ No newline at end of file
diff --git "a/docs/\355\225\234\352\265\255\354\226\264/README.md" "b/docs/\355\225\234\352\265\255\354\226\264/README.md"
deleted file mode 100644
index 4d7a7ce2..00000000
--- "a/docs/\355\225\234\352\265\255\354\226\264/README.md"
+++ /dev/null
@@ -1,24 +0,0 @@
-# 디스코도 문서에 오신것을 환영합니다!
-
-디스코도는 강화된 디스코드 오디오 플레이어입니다.
-
-## 기능
-- 독립적인 오디오 노드
-- Youtube 추천영상 자동재생
-- 크로스페이드 및 오디오 효과
-- 싱크 Youtube 영상 자막
-
-## 호환되는 소스
-
-+ [youtube_dl](https://github.com/ytdl-org/youtube-dl)로 추출 가능한 모든 서비스
-+ [libav](https://libav.org/)로 디묵싱 가능한 모든 포멧
-
-## 클라이언트 라이브러리
-
-* [discodo](https://github.com/kijk2869/discodo) (Python, discord.py)
-* ~~[discodo.js](개발중) (Node.js, discord.js)~~ **개발중**
-
-## 본문
-
-- [클라이언트](./클라이언트)
-- [서버](./서버)
diff --git "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/README.md" "b/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/README.md"
deleted file mode 100644
index 2893e43c..00000000
--- "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/README.md"
+++ /dev/null
@@ -1,25 +0,0 @@
-# 디스코도 서버 문서
-
-## 기본 주소
-
-Restful
-```
- http://<host>:<port>/
-```
-Websocket
-```
- ws://<host>:<port>/ws
-```
-
-## 본문
-
-### 서문
-- [설치](./설치.md)
-- [연결](./연결.md)
-
-### Websocket
-- [페이로드](./페이로드.md)
-- [이벤트](./이벤트.md)
-
-### Restful
-- [Restful API](./restful.md)
diff --git "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/restful.md" "b/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/restful.md"
deleted file mode 100644
index 7336b2c0..00000000
--- "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/restful.md"
+++ /dev/null
@@ -1,491 +0,0 @@
-# Restful API
-
-## Routes
-
-### GET /status
-
-서버의 자원 상태를 가져옵니다.
-
-만약 플레이어 개수를 확인하고 싶다면, `GET_STAT` 웹소켓 페이로드를 사용해야 합니다.
-
-#### Response
-
-```json
-{
-    "UsedMemory": 0,
-    "TotalMemory": 0,
-    "ProcessLoad": 0,
-    "TotalLoad": 0,
-    "Cores": 0,
-    "Threads": 0,
-    "NetworkInbound": 0,
-    "NetworkOutbound": 0
-}
-```
-
-### GET /planner
-
-Route Planner의 정보를 가져옵니다.
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-
-#### Response
-
-```json
-{
-    "ipBlocks": [
-        {
-            "version": 4,
-            "broadcast_address": "0.0.0.0",
-            "size": 1
-        }
-    ],
-    "failedAddresses": [
-        {
-            "address": "0.0.0.0",
-            "status": 429,
-            "failed_at": 0
-        }
-    ]
-}
-```
-
-### POST /planner/unmark
-
-Unmark an address on planner's failed list.
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-
-#### Parameters
-
-|Key|Type|Description|
-|---|----|-----------|
-|address|string|address to unmark|
-
-#### Response
-
-```json
-{"status": 200}
-```
-
-### POST /planner/unmark/all
-
-Unmark all address on planner's failed list.
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-
-#### Response
-
-```json
-{"status": 200}
-```
-
-### GET /getSource
-
-Get the information of the video searched on Youtube
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-
-#### Parameters
-
-|Key|Type|Description|
-|---|----|-----------|
-|query|string|query to search on youtube|
-
-#### Response
-
-```json5
-{
-    "source": {
-        // Source Object
-    }
-}
-```
-
-### POST /putSource
-
-Put source object to queue
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json5
-{
-    "source": {
-        // Source Object
-    }
-}
-```
-
-#### Response
-
-```json
-{
-    "index": 0
-}
-```
-
-### POST /loadSource
-
-Load query from youtube and put source object to queue
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "query": "Something"
-}
-```
-
-#### Response
-
-```json5
-{
-    "source": {
-        // Source Object
-    }
-}
-```
-
-### POST /setVolume
-
-Set volume of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json5
-{
-    "volume": 1.0 // 0.0 ~ 2.0
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /setCrossfade
-
-Set crossfade of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "crossfade": 10.0
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /setGapless
-
-Set gapless of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "gapless": false
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /setAutoplay
-
-Set autoplay of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "autoplay": true
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /setFilter
-
-Set filter of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json5
-{
-    "filter": {
-        // libavfilter object
-    }
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /seek
-
-Seek source to offset of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "offset": 10.0
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /skip
-
-Skip source of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Data
-
-```json
-{
-    "offset": 1
-}
-```
-
-#### Response
-
-Empty `204`
-
-### POST /pause
-
-Pause source of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-Empty `204`
-
-### POST /resume
-
-Resume source of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-Empty `204`
-
-### POST /shuffle
-
-Shuffle the queue of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-```json5
-{
-    "entries": [
-        {
-            // Source Object
-        }
-    ]
-}
-```
-
-### POST /remove
-
-Remove source object from the queue of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-```json5
-{
-    "removed": 1,
-    "entries": [
-        {
-            // Source Object
-        }
-    ]
-}
-```
-
-### GET /state
-
-Get state of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-```json
-{
-    "op": "getState",
-    "d": {
-        "id": "...",
-        "guild_id": "...",
-        "channel_id": "...",
-        "state": "...",
-        "current": {},
-        "duration": 0.0,
-        "position": 0.0,
-        "remain": 0.0,
-        "options": {
-            "autoplay": true,
-            "volume": 1.0,
-            "crossfade": 10.0,
-            "gapless": false,
-            "filter": {}
-        }
-    }
-}
-```
-
-### GET /queue
-
-Get state of Guild ID
-
-#### Headers
-
-|Key|Type|Description|
-|---|----|-----------|
-|Authorization|string|Password for discodo server|
-|User-ID|integer|Discord User ID|
-|Guild-ID|integer|Discord Guild ID|
-
-#### Response
-
-```json5
-{
-    "entries": [
-        {
-            // Source Object
-        }
-    ]
-}
-```
diff --git "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\354\204\244\354\271\230.md" "b/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\354\204\244\354\271\230.md"
deleted file mode 100644
index 4bf41ee4..00000000
--- "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\354\204\244\354\271\230.md"
+++ /dev/null
@@ -1,100 +0,0 @@
-# 서문
-
-## 요구사항
-
-디스코도는 **Python 3.7+** 에서 동작합니다.
-
-더 이른 버전들은 작동하지 않을 수 있습니다.
-
-## 설치
-
-이 라이브러리는 [**PyPI**](https://pypi.org/project/discodo/)에서 설치할 수 있습니다:
-
-```sh
-python3 -m pip install --upgrade discodo
-```
-
-리눅스 환경에서는, 일부 라이브러리가 더 필요합니다:
-
-- python3-dev
-- libopus-dev
-- libnacl-dev
-
-## 실행
-
-추가적인 옵션은 `--help` 플래그로 확인할 수 있습니다.
-
-```sh
-python3 -m discodo [-h] [--config CONFIG] [--host HOST] [--port PORT] [--auth AUTH] [--ws-interval WS_INTERVAL]
-               [--ws-timeout WS_TIMEOUT] [--ip IP] [--exclude-ip EXCLUDE_IP] [--default-volume DEFAULT_VOLUME]
-               [--default-crossfade DEFAULT_CROSSFADE] [--default-gapless DEFAULT_GAPLESS]
-               [--default-autoplay DEFAULT_AUTOPLAY] [--bufferlimit BUFFERLIMIT] [--preload PRELOAD]
-               [--timeout TIMEOUT] [--verbose]
-```
-
-## 설정
-
-```
-선택 인자:
-  -h, --help            이 도움말 메세지를 띄웁니다
-  --version             디스코도의 버전을 띄웁니다
-  --config CONFIG       설정 JSON 파일 경로 (기본: None)
-
-서버 설정:
-  --host HOST, -H HOST  서버가 바인딩할 호스트 이름 (기본: 0.0.0.0)
-  --port PORT, -P PORT  서버가 바인딩할 포트 (기본: 8000)
-  --auth AUTH, -A AUTH  서버의 비밀번호 (기본: hellodiscodo)
-  --ws-interval WS_INTERVAL
-                        디스코도 서버와 클라이언트사이의 Heartbeat 간격 (기본: 15)
-  --ws-timeout WS_TIMEOUT
-                        Heartbeat 답장을 기다릴 시간 (기본: 60)
-
-네트워크 설정:
-  --ip IP               추출에 사용할 IP 블럭
-  --exclude-ip EXCLUDE_IP
-                        추출에 사용하지 않을 IP
-
-플레이어 설정:
-  --default-volume DEFAULT_VOLUME
-                        플레이어의 기본 볼륨 (기본: 100)
-  --default-crossfade DEFAULT_CROSSFADE
-                        플레이어의 기본 크로스페이드 시간 (기본: 10.0)
-  --default-autoplay DEFAULT_AUTOPLAY
-                        플레이어의 기본 추천영상 자동재생 여부 (기본: True)
-  --bufferlimit BUFFERLIMIT
-                        버퍼에 저장할 파일의 길이 (기본: 5)
-  --preload PRELOAD     다음곡을 미리 로드할 시간 (기본: 10)
-  --timeout TIMEOUT     디스코드 음성 서버 연결 대기시간 (기본: 300)
-
-로깅 설정:
-  --verbose, -v         디버그 모드를 활성화합니다.
-```
-
-### 설정파일
-
-`--config` 플래그로 `JSON` 파일 경로를 전달해야 합니다.
-
-```json
-{
-    "HOST": "0.0.0.0",
-    "PORT": 8000,
-    "PASSWORD": "hellodiscodo",
-    "HANDSHAKE_INTERVAL": 15,
-    "HANDSHAKE_TIMEOUT": 60,
-    "IPBLOCKS": [],
-    "EXCLUDEIPS": [],
-    "DEFAULT_AUTOPLAY": true,
-    "DEFAULT_VOLUME": 1,
-    "DEFAULT_CROSSFADE": 10,
-    "DEFAULT_GAPLESS": false,
-    "BUFFERLIMIT": 5,
-    "PRELOAD_TIME": 10,
-    "VCTIMEOUT": 300,
-    "ENABLED_EXT_RESOLVER": [
-        "melon",
-        "spotify"
-    ],
-    "SPOTIFY_ID": null,
-    "SPOTIFY_SECRET": null
-}
-```
diff --git "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\354\227\260\352\262\260.md" "b/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\354\227\260\352\262\260.md"
deleted file mode 100644
index 77d72324..00000000
--- "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\354\227\260\352\262\260.md"
+++ /dev/null
@@ -1,97 +0,0 @@
-# 웹소켓 연결
-
-## 디스코도에 연결하기
-
-### 연결
-
-#### 웹소켓 헤더
-
-|이름|자료형|설명|
-|---|----|-----------|
-|Authorization|문자열|디스코도 서버의 비밀번호|
-
-헤더가 누락되거나 일치하지 않는다면, 클라이언트는 FORBIDDEN 이벤트를 수신합니다.
-
-#### FORBIDDEN 예재
-
-```json
-{
-    "op": "FORBIDDEN",
-    "d": "연결이 거부된 사유"
-}
-```
-
-연결에 성공하면, 클라이언트는 Heartbeat 주기가 담긴 HELLO 이벤트를 수신합니다.
-
-#### HELLO 예제
-
-```json
-{
-    "op": "HELLO",
-    "d": {
-        "heartbeat_interval": 15.0
-    }
-}
-```
-
-### Heartbeat 전송
-
-HELLO 이벤트를 수신한 이후에는, 클라이언트는 연결이 종료될때까지 매 `heartbeat_interval`초 마다 HEARTBEAT 페이로드를 전송하기 시작해야 합니다.
-
-#### HEARTBEAT 페이로드 예제
-
-```json5
-{
-    "op": "HEARTBEAT",
-    "d": 1596417937 // 타임스탬프
-}
-```
-
-페이로드 데이터 (`d`)는 nullable입니다, 서버는 그것을 재전송합니다.
-
-#### HEARTBEAT_ACK 예제
-
-```json5
-{
-    "op": "HEARTBEAT_ACK",
-    "d": 1596417937 // 타임스탬프
-}
-```
-
-### 식별
-
-클라이언트는 오디오 매니저를 설정하기 위해서 IDENTIFY 페이로드를 전송해야 합니다.
-
-#### IDENTIFY 페이로드 예제
-
-```json
-{
-    "op": "IDENTIFY",
-    "d": {
-        "user_id": "봇 ID"
-    }
-}
-```
-
-### 복원
-
-VC_TIMEOUT전에 같은 유저 아이디가 연결된다면, 매니저가 복원됩니다.
-
-#### RESUMED 예제
-
-```json5
-{
-    "op": "RESUMED",
-    "d": {
-        "voice_clients": [
-            [0, 0] // guild_id, voicechannel_id(can be null)
-        ]
-    }
-}
-```
-
-클라이언트가 RESUMED 이벤트를 수신한다면, 음성 채널에 재연결해야합니다.
-
-### 연결 해제
-
-연결이 종료된다면, 서버는 매니저와 재생중인 소스들을 정리합니다.
diff --git "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\354\235\264\353\262\244\355\212\270.md" "b/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\354\235\264\353\262\244\355\212\270.md"
deleted file mode 100644
index 1cd6372e..00000000
--- "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\354\235\264\353\262\244\355\212\270.md"
+++ /dev/null
@@ -1,326 +0,0 @@
-# 웹소켓 이벤트
-
-### HEARTBEAT_ACK
-
-```json5
-{
-    "op": "HEARTBEAT_ACK",
-    "d": 1596417937 // 타임스탬프
-}
-```
-
-### STAT
-
-```json
-{
-    "UsedMemory": 0,
-    "TotalMemory": 0,
-    "ProcessLoad": 0,
-    "TotalLoad": 0,
-    "Cores": 0,
-    "Threads": 0,
-    "NetworkInbound": 0,
-    "NetworkOutbound": 0,
-    "TotalPlayer": 0
-}
-```
-
-### IDENTIFIED
-
-```json
-{
-    "op": "IDENTIFIED",
-    "d": "ClientManager initialized."
-}
-```
-
-## Audio Player
-
-음성채널에 연결된 이후에만 전송됩니다.
-
-### VC_CREATED
-
-```json
-{
-    "op": "VC_CREATED",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID"
-    }
-}
-```
-
-### VC_DESTROYED
-
-```json
-{
-    "op": "VC_DESTROYED",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID"
-    }
-}
-```
-
-### setVolume
-
-```json5
-{
-    "op": "setVolume",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "volume": 1.0 // 0.0 ~ 2.0
-    }
-}
-```
-
-### setCrossfade
-
-```json
-{
-    "op": "setCrossfade",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "crossfade": 10.0
-    }
-}
-```
-
-### setAutoplay
-
-```json
-{
-    "op": "setAutoplay",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "autoplay": true
-    }
-}
-```
-
-### setFilter
-
-```json5
-{
-    "op": "setFilter",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "filter": {
-            // libavfilter 객체
-        }
-    }
-}
-```
-### getSource
-
-```json5
-{
-    "op": "getSource",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "source": {
-            // Source 객체
-        }
-    }
-}
-```
-
-### loadSource
-
-```json5
-{
-    "op": "loadSource",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "source": {
-            // Source 객체
-        }
-    }
-}
-```
-
-### putSource
-
-```json5
-{
-    "op": "putSong",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "source": {
-            // Source 객체
-        }
-    }
-}
-```
-
-### REQUIRE_NEXT_SOURCE
-
-**만약 `autoplay` 가 활성화되있다면, 추천 영상이 이 이벤트 이후에 자동으로 추가됩니다.**
-
-```json5
-{
-    "op": "REQUIRE_NEXT_SOURCE",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "current": {
-            // Source 객체
-        }
-    }
-}
-```
-
-### skip
-
-```json
-{
-    "op": "skip",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "remain": 0
-    }
-}
-```
-
-### seek
-
-```json
-{
-    "op": "seek",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "offset": 0
-    }
-}
-```
-
-### Queue
-
-```json5
-{
-    "op": "Queue",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "entries": [
-            // source 객체
-        ]
-    }
-}
-```
-
-### State
-
-```json5
-{
-    "op": "getState",
-    "d": {
-        "id": "...",
-        "guild_id": "...",
-        "channel_id": "...",
-        "state": "...",
-        "current": {},
-        "duration": 0.0,
-        "position": 0.0,
-        "remain": 0.0,
-        "options": {
-            "autoplay": true,
-            "volume": 1.0,
-            "crossfade": 10.0,
-            "filter": {}
-        }
-    }
-}
-```
-
-### pause
-
-```json
-{
-    "op": "pause",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "state": "paused"
-    }
-}
-```
-
-### resume
-
-```json
-{
-    "op": "pause",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "state": "playing"
-    }
-}
-```
-
-### shuffle
-
-```json5
-{
-    "op": "shuffle",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "entries": [
-            // Source 객체
-        ]
-    }
-}
-```
-
-### remove
-
-```json5
-{
-    "op": "remove",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "removed": {
-            // Source 객체
-        },
-        "entries": [
-            // Source 객체
-        ]
-    }
-}
-```
-
-### requestSubtitle
-
-```json
-{
-    "op": "requestSubtitle",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "identify": "58b8b68f-e938-4699-b4ad-ffa262bf903d",
-        "url": "..."
-    }
-}
-```
-
-### Subtitle
-
-```json
-{
-    "op": "Subtitle",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "identify": "58b8b68f-e938-4699-b4ad-ffa262bf903d",
-        "previous": "이전 자막",
-        "current": "현재 자막",
-        "next": "다음 자막"
-    }
-}
-```
-
-### subtitleDone
-
-```json
-{
-    "op": "subtitleDone",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "identify": "58b8b68f-e938-4699-b4ad-ffa262bf903d",
-        "url": "..."
-    }
-}
-```
diff --git "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\355\216\230\354\235\264\353\241\234\353\223\234.md" "b/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\355\216\230\354\235\264\353\241\234\353\223\234.md"
deleted file mode 100644
index 7d9a2389..00000000
--- "a/docs/\355\225\234\352\265\255\354\226\264/\354\204\234\353\262\204/\355\216\230\354\235\264\353\241\234\353\223\234.md"
+++ /dev/null
@@ -1,284 +0,0 @@
-# 웹소켓 페이로드
-
-#### 페이로드 구조
-
-|이름|자료형|설명|
-|---|----|-----------|
-|op|문자열|페이로드의 이름|
-|d|혼합 (대부분 JSON)|페이로드의 데이터|
-
-### 페이로드 전송하기
-
-#### 페이로드 전송 예제
-
-```json
-{
-    "op": "페이로드 이름",
-    "d": {}
-}
-```
-
-### 페이로드의 오류 반환
-
-#### 페이로드 예외 예제
-
-```json
-{
-    "op": "페이로드 이름",
-    "d": {
-        "traceback": "파이썬 예외"
-    }
-}
-```
-
-### HEARTBEAT
-
-```json5
-{
-    "op": "HEARTBEAT",
-    "d": 1596417937 // 타임스탬프
-}
-```
-
-### GET_STAT
-
-```json5
-{
-    "op": "GET_STAT"
-}
-```
-
-### IDENTIFY
-
-```json
-{
-    "op": "IDENTIFY",
-    "d": {
-        "user_id": "봇 ID",
-        "session_id": "디스코드 연결의 세션 ID"
-    }
-}
-```
-
-### DISCORD_EVENT
-
-```json5
-{
-    "op": "DISCORD_EVENT",
-    "d": {
-        // 디스코드 소켓 이벤트
-    }
-}
-```
-**`READY`, `RESUME`, `VOICE_STATE_UPDATE`, `VOICE_SERVER_UPDATE` 페이로드만이 필요합니다.**
-
-## 오디오 플레이어
-
-음성 채널에 연결한 이후에만 사용할 수 있습니다.
-
-### setVolume
-
-```json5
-{
-    "op": "setVolume",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "volume": 1 // 0.0~2.0
-    }
-}
-```
-
-### setCrossfade
-
-```json5
-{
-    "op": "setCrossfade",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "crossfade": 10 // seconds
-    }
-}
-```
-
-### setAutoplay
-
-```json5
-{
-    "op": "setAutoplay",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "autoplay": true
-    }
-}
-```
-
-### setFilter
-
-```json5
-{
-    "op": "setFilter",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "filter": {
-            // libavfilter 객체
-        }
-    }
-}
-```
-
-### getSource
-
-```json
-{
-    "op": "getSource",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "query": "검색할 문자"
-    }
-}
-```
-
-### loadSource
-
-```json
-{
-    "op": "loadSource",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "query": "검색할 문자"
-    }
-}
-```
-
-### putSong
-
-```json5
-{
-    "op": "putSource",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "source": {
-            // Source 객체
-        }
-    }
-}
-```
-
-### skip
-
-```json5
-{
-    "op": "skip",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "offset": 1 // 건너뛸 소스 개수
-    }
-}
-```
-
-### seek
-
-```json5
-{
-    "op": "seek",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "offset": 10 // 초
-    }
-}
-```
-
-### getQueue
-
-```json
-{
-    "op": "getQueue",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID"
-    }
-}
-```
-
-### getState
-
-```json
-{
-    "op": "getState",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID"
-    }
-}
-```
-
-### pause
-
-```json
-{
-    "op": "pause",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID"
-    }
-}
-```
-
-### resume
-
-```json
-{
-    "op": "resume",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID"
-    }
-}
-```
-
-### shuffle
-
-```json
-{
-    "op": "shuffle",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID"
-    }
-}
-```
-
-### remove
-
-```json5
-{
-    "op": "remove",
-    "d": {
-        "guild_id": "오디오 플레이어의 서버 ID",
-        "index": 1 // 삭제할 소스 번호
-    }
-}
-```
-
-### requestSubtitle
-
-`lang` 나 `url` 중 하나가 필요합니다.
-
-디스코도는 `smi` 와 `srv1` 가 호환됩니다.
-
-```json5
-{
-    "op": "requestSubtitle",
-    "d": {
-        "guild_id": "삭제할 소스 번호",
-        "lang": "en",
-        "url": "..."
-    }
-}
-```
-
-### VC_DESTROY
-
-```json
-{
-    "op": "VC_DESTROY",
-    "d": {
-        "guild_id": "삭제할 소스 번호"
-    }
-}
-```
diff --git a/example/advanced.py b/example/advanced.py
new file mode 100644
index 00000000..8c5d0e9b
--- /dev/null
+++ b/example/advanced.py
@@ -0,0 +1,479 @@
+import asyncio
+import contextlib
+import itertools
+import logging
+import math
+
+import discord
+from discord.ext import commands
+from rich.logging import RichHandler
+
+import discodo
+
+NEWLINE = "\n"
+
+logging.basicConfig(
+    level=logging.DEBUG,
+    format="%(name)s :\t%(message)s",
+    handlers=[RichHandler(rich_tracebacks=True)],
+)
+logging.getLogger("discord").setLevel(logging.INFO)
+
+
+def getProgress(value, Total):
+    position_front = round(value / Total * 10)
+    position_back = 10 - position_front
+
+    return "▬" * position_front + "🔘" + "▬" * position_back
+
+
+def formatDuration(seconds):
+    seconds = int(seconds)
+    minute, second = divmod(seconds, 60)
+    hour, minute = divmod(minute, 60)
+
+    return (f"{hour:02}:" if hour else "") + f"{minute:02}:{second:02}"
+
+
+class Bot(commands.Bot):
+    def __init__(self, command_prefix, **options):
+        super().__init__(command_prefix, **options)
+
+        self.Audio = discodo.DPYClient(self)
+        self.Audio.registerNode(region="LOCAL")
+
+        self.add_cog(Music(self))
+
+    async def pagination(self, ctx, callback, limit):
+        position = 0
+
+        message = await ctx.reply(**callback(position))
+
+        async def _add_emojis():
+            with contextlib.suppress(Exception):
+                for emoji in ["◀", "⏹", "▶"]:
+                    await message.add_reaction(emoji)
+
+        self.loop.create_task(_add_emojis())
+
+        while not self.is_closed():
+            try:
+                reaction, user = await self.wait_for(
+                    "reaction_add",
+                    check=lambda reaction, user: user == ctx.author
+                    and reaction.message.id == message.id
+                    and reaction.emoji in ["◀", "⏹", "▶"],
+                    timeout=30,
+                )
+            except asyncio.TimeoutError:
+                with contextlib.suppress(Exception):
+                    await message.clear_reactions()
+                break
+
+            if reaction.emoji == "◀" and position > 0:
+                position -= 1
+            elif reaction.emoji == "⏹":
+                with contextlib.suppress(Exception):
+                    await message.clear_reactions()
+                break
+            elif reaction.emoji == "▶" and position < limit:
+                position += 1
+
+            await message.edit(**callback(position))
+
+            with contextlib.suppress(discord.Forbidden):
+                await message.remove_reaction(reaction.emoji, user)
+
+
+class SubtitleCallback:
+    def __init__(self, channel: discord.TextChannel):
+        self.loop = asyncio.get_event_loop()
+
+        self._message: discord.Message = None
+        self.channel: discord.TextChannel = channel
+
+    async def callback(self, subtitle: str) -> None:
+        if not self._message or self.channel.last_message.id != self._message.id:
+            if self._message:
+                self.loop.create_task(self._message.delete())
+
+            self._message = await self.channel.send(
+                f'{subtitle.get("previous", "")}\n> {subtitle["current"]}\n{subtitle.get("next") or ""}'
+            )
+        else:
+            await self._message.edit(
+                content=f'{subtitle.get("previous", "")}\n> {subtitle["current"].replace(NEWLINE, f"{NEWLINE}> ")}\n{subtitle.get("next") or ""}'
+            )
+
+
+class Music(commands.Cog):
+    def __init__(self, Bot):
+        self.Bot = Bot
+        self.Audio = Bot.Audio
+
+        self.Audio.dispatcher.on("SOURCE_START", self.on_source_start)
+
+    async def on_source_start(self, voice_client, data):
+        context = await voice_client.getContext()
+        channel = await self.Bot.fetch_channel(context.get("text_channel"))
+
+        await channel.send(f"Now playing {data['source'].title}...")
+
+    @commands.is_owner()
+    @commands.command()
+    async def connectnodes(self, ctx):
+        """Connect to the discodo nodes."""
+
+        def getText():
+            return f"""
+> **Discodo Node Connection**
+>
+{NEWLINE.join(map(lambda x: '> '+('🛠️  `' if not x.is_connected else '✅  `') + x.region + '`', ctx.bot.Audio.Nodes))}
+"""
+
+        message = await ctx.send(getText())
+
+        for Node in ctx.bot.Audio.Nodes:
+            try:
+                await Node.connect()
+            finally:
+                await message.edit(content=getText())
+
+    @commands.command()
+    async def join(self, ctx):
+        """Connect to the voice channel."""
+
+        if not ctx.author.voice:
+            return await ctx.reply("Join the voice channel first.")
+
+        try:
+            VC = await self.Audio.connect(ctx.author.voice.channel)
+        except discodo.NodeNotConnected:
+            return await ctx.send("There is no available node.")
+        except asyncio.TimeoutError:
+            return await ctx.send("The connection is not established in 10 seconds.")
+
+        await VC.setContext({"text_channel": ctx.channel.id})
+
+        await ctx.reply(f"Successfully connected to {ctx.author.voice.channel}")
+
+    @commands.command()
+    async def play(self, ctx, *, query: str):
+        """Play a source with the query."""
+
+        if not ctx.voice_client:
+            await ctx.invoke(self.join)
+
+        Sources = await ctx.voice_client.loadSource(query)
+        if not Sources:
+            return await ctx.reply("There is no search result.")
+
+        if isinstance(Sources, list):
+            await ctx.reply(f"Added {len(Sources)} sources to the queue.")
+        else:
+            await ctx.reply(f"Added {Sources.title} sources to the queue.")
+
+        await asyncio.gather(
+            *list(
+                map(
+                    lambda x: x.setContext({"requester": ctx.author.id}),
+                    Sources if isinstance(Sources, list) else [Sources],
+                )
+            )
+        )
+
+    @commands.command()
+    async def pause(self, ctx):
+        """Pause the player."""
+
+        if not ctx.voice_client:
+            return await ctx.reply("I'm not playing anything.")
+
+        await ctx.voice_client.pause()
+
+        await ctx.reply("Paused playing.")
+
+    @commands.command()
+    async def resume(self, ctx):
+        """Resume the player."""
+
+        if not ctx.voice_client:
+            return await ctx.reply("I'm not playing anything.")
+
+        await ctx.voice_client.resume()
+
+        await ctx.reply("Resumed playing.")
+
+    @commands.command()
+    async def skip(self, ctx):
+        """Skip the currently playing."""
+
+        if not ctx.voice_client:
+            return await ctx.reply("I'm not playing anything.")
+
+        Current = await ctx.voice_client.getCurrent()
+
+        if ctx.author.id == Current["context"].get("requester"):
+            await ctx.reply("The source requester has skipped the source.")
+
+            return await ctx.voice_client.skip()
+
+        if "skip_votes" not in Current["context"]:
+            Current["context"]["skip_votes"] = []
+
+        if ctx.author.id in Current["context"]["skip_votes"]:
+            return await ctx.reply("⚠️ You already have voted to skip this source.")
+
+        Current["context"]["skip_votes"].append(ctx.author.id)
+
+        await Current.setContext(Current["context"])
+
+        await ctx.reply("✅ You have voted to skip this source.")
+
+        channel = await self.Bot.fetch_channel(ctx.voice_client.channel_id)
+
+        if len(Current["context"]["skip_votes"]) >= round(
+            (len(channel.members) - 1) / 2
+        ):
+            await ctx.send(
+                "More than half of the votes for skipping music, skipping the source."
+            )
+
+            await ctx.voice_client.skip()
+
+    @commands.command()
+    async def stop(self, ctx):
+        """Stop the player and clear the queue."""
+
+        if not ctx.voice_client:
+            return await ctx.reply("I'm not playing anything.")
+
+        await ctx.voice_client.destroy()
+        await ctx.reply("Stopped the player and cleared the queue.")
+
+    @commands.command()
+    async def seek(self, ctx, offset: int):
+        """Seek the source."""
+
+        if not ctx.voice_client:
+            return await ctx.reply("I'm not playing anything.")
+
+        Current = await ctx.voice_client.getCurrent()
+
+        if not 0 < offset < Current.duration:
+            return await ctx.reply(
+                f"Please enter the value between 0 and {Current.duration}."
+            )
+
+        await ctx.voice_client.seek(offset)
+
+        await ctx.reply(f"Seeked to {offset}")
+
+    @commands.command(aliases=["vol"])
+    async def volume(self, ctx, volume: int):
+        """Change the volume."""
+
+        if not ctx.voice_client:
+            return await ctx.reply("I'm not playing anything.")
+
+        if not 0 < volume < 101:
+            return await ctx.reply("Please enter the value between 1 and 100.")
+
+        await ctx.voice_client.setVolume(volume / 100)
+        await ctx.reply(f"Set the volume to {volume}%")
+
+    @commands.command(aliases=["eq"])
+    async def equalizer(self, ctx, preset: str = None):
+        """Change the equalizer preset."""
+
+        if not ctx.voice_client:
+            return await ctx.send("I'm not playing anything.")
+
+        PRESETS = {
+            "POP": {
+                63: 0.0,
+                125: 0.0,
+                250: 0.0,
+                500: 0.0,
+                1000: 2.0,
+                2000: 2.0,
+                4000: 3.5,
+                8000: -2.0,
+                16000: -4.0,
+            },
+            "CLASSIC": {
+                63: 0.0,
+                125: 0.0,
+                250: -1.0,
+                500: -6.0,
+                1000: 0.0,
+                2000: 1.0,
+                4000: 1.0,
+                8000: 0.0,
+                16000: 6.0,
+            },
+            "JAZZ": {
+                63: 0.0,
+                125: 0.0,
+                250: 2.5,
+                500: 5.0,
+                1000: -6.0,
+                2000: -2.0,
+                4000: -1.0,
+                8000: 2.0,
+                16000: -1.0,
+            },
+            "ROCK": {
+                63: 0.0,
+                125: 0.0,
+                250: 1.0,
+                500: 3.0,
+                1000: -10.0,
+                2000: -2.0,
+                4000: -1.0,
+                8000: 3.0,
+                16000: 3.0,
+            },
+            "FLAT": {
+                63: 0.0,
+                125: 0.0,
+                250: 0.0,
+                500: 0.0,
+                1000: 0.0,
+                2000: 0.0,
+                4000: 0.0,
+                8000: 0.0,
+                16000: 0.0,
+            },
+        }
+
+        selectedPreset = PRESETS.get(preset.upper() if preset else None)
+
+        if not selectedPreset:
+            return await ctx.reply(
+                f"Invalid preset. Valid presets are{NEWLINE*2}{NEWLINE.join(PRESETS.keys())}"
+            )
+
+        State = await ctx.voice_client.getState()
+
+        State["options"]["filter"]["anequalizer"] = "|".join(
+            [
+                f"c0 f={Frequency} w=100 g={Gain}|c1 f={Frequency} w=100 g={Gain}"
+                for Frequency, Gain in selectedPreset.items()
+                if Gain != 0
+            ]
+        )
+
+        await ctx.voice_client.setFilter(State["options"]["filter"])
+        await ctx.reply(f"Changed equalizer preset to {preset}")
+
+    @commands.command(aliases=["q"])
+    async def queue(self, ctx):
+        """Show the players queue."""
+
+        if not ctx.voice_client:
+            return await ctx.reply("I'm not playing anything.")
+
+        if not ctx.voice_client.Queue:
+            return await ctx.invoke(self.nowplaying)  # TODO
+
+        def callback(position):
+            start = position * 8
+
+            return {
+                "embed": discord.Embed(
+                    title="Queue",
+                    description=NEWLINE.join(
+                        map(
+                            lambda x: f"{x[0]}. {x[1].title}",
+                            itertools.islice(
+                                enumerate(ctx.voice_client.Queue, 1), start, start + 8
+                            ),
+                        )
+                    ),
+                )
+            }
+
+        await self.Bot.pagination(
+            ctx, callback, limit=math.floor(len(ctx.voice_client.Queue) / 8)
+        )
+
+    @commands.command(aliases=["np"])
+    async def nowplaying(self, ctx):
+        """Show the current playing."""
+
+        if not ctx.voice_client:
+            return await ctx.reply("I'm not playing anything.")
+
+        if not ctx.voice_client.current:
+            embed = discord.Embed(title="I'm not playing anything!")
+        else:
+            Chapters = list(
+                filter(
+                    lambda x: x["start_time"]
+                    <= ctx.voice_client.current.position
+                    < x["end_time"],
+                    ctx.voice_client.current.get("chapters") or [],
+                )
+            )
+            Chapter = Chapters[0] if Chapters else None
+
+            STATE_EMOJI = {"playing": "▶️", "paused": "⏸️", "stopped": "⏹️"}
+
+            embed = discord.Embed(
+                title=ctx.voice_client.current.title,
+                url=ctx.voice_client.current.webpage_url,
+                description=(
+                    (
+                        f"- `[{formatDuration(Chapter['start_time'])} ~ {formatDuration(Chapter['end_time'])}]` **{Chapter['title']}**\n\n"
+                        if Chapter
+                        else ""
+                    )
+                    + f"> ❤️ Audio Node: **{ctx.voice_client.Node.region}**\n"
+                    + f"{STATE_EMOJI[ctx.voice_client.current]} "
+                    + getProgress(
+                        ctx.voice_client.current.position, ctx.voice_client.duration
+                    )
+                    + f" `[{formatDuration(ctx.voice_client.current.position)}/{formatDuration(ctx.voice_client.current.duration)}]`"
+                    + f" 🔉 **{round(ctx.voice_client.volume * 100)}%**"
+                ),
+            )
+            if ctx.voice_client.current.thumbnail:
+                embed.set_thumbnail(url=ctx.voice_client.current.thumbnail)
+
+            embed.set_footer(
+                text=f"From {ctx.voice_client.current.uploader} | {len(ctx.voice_client.Queue)} sources left"
+            )
+
+        embed.colour = ctx.guild.me.colour
+
+        await ctx.reply(embed=embed)
+
+    @commands.command(aliases=["subtitle", "lyrics"])
+    async def subtitles(self, ctx, value: str = None):
+        """Print the synced subtitle of the currently playing source."""
+        usableSubtitles = (
+            ctx.voice_client.current.get("subtitles", {}).keys()
+            if ctx.voice_client.current
+            else []
+        )
+
+        if not value:
+            return await ctx.reply(
+                f"> Usable subtitles: {' '.join(map(lambda x: f'`{x}`', usableSubtitles))}"
+            )
+
+        if value and value not in usableSubtitles:
+            return await ctx.reply(
+                f"> ❎  Couldn't find `{value}` subtitle.\n> \n> Usable subtitles: {' '.join(map(lambda x: f'`{x}`', usableSubtitles))}"
+            )
+
+        await ctx.voice_client.getSubtitle(
+            lang=value, callback=SubtitleCallback(ctx.channel).callback
+        )
+
+        await ctx.reply(f"> ➡️  I'll send {f'`{value}` ' if value else ''} subtitle!")
+
+
+if __name__ == "__main__":
+    Bot(command_prefix="!!").run("your discord bot token here")
diff --git a/example/basic.py b/example/basic.py
new file mode 100644
index 00000000..90b1ca4a
--- /dev/null
+++ b/example/basic.py
@@ -0,0 +1,65 @@
+import discord
+
+import discodo
+
+client = discord.Client()
+codo = discodo.DPYClient(client)
+
+
+@client.event
+async def on_ready():
+    print(f"I logged in as {client.user} (client.user.id)")
+
+
+@codo.event("SOURCE_START")
+async def sendPlaying(VC, Data):
+    await VC.channel.send(f"I'm now playing {Data['source']['title']}")
+
+
+@client.event
+async def on_message(message):
+    if message.author == client.user:
+        return
+
+    if message.content.startswith("!join"):
+        if not message.author.voice:
+            return await message.channel.send("Join the voice channel first.")
+
+        await codo.connect(message.author.voice.channel)
+
+        return await message.channel.send(
+            f"I connected to {message.author.voice.channel.mention}"
+        )
+
+    if message.content.startswith("!play"):
+        VC = codo.getVC(message.guild, safe=True)
+
+        if not VC:
+            return await message.channel.send("Please type `!join` first.")
+
+        if not hasattr(VC, "channel"):
+            VC.channel = message.channel
+
+        source = await VC.loadSource(message.content[5:].strip())
+
+        if isinstance(source, list):
+            return await message.channel.send(
+                f"{len(source) - 1} songs except {source[0].title} added."
+            )
+        return await message.channel.send(f"{source.title} added.")
+
+    if message.content.startswith("!stop"):
+        VC = codo.getVC(message.guild, safe=True)
+
+        if not VC:
+            return await message.channel.send(
+                "I'm not connected to any voice channel now."
+            )
+
+        await VC.destroy()
+
+        return await message.channel.send("I stopped the player and cleaned the queue.")
+
+
+codo.registerNode()
+client.run("your discord bot token here")
diff --git a/example/remote/config.inc.json b/example/config.inc.json
similarity index 100%
rename from example/remote/config.inc.json
rename to example/config.inc.json
diff --git a/example/local/example1.py b/example/local/example1.py
deleted file mode 100644
index da66ffef..00000000
--- a/example/local/example1.py
+++ /dev/null
@@ -1,197 +0,0 @@
-"""
-
-This example isn't recommended in production.
-Just use it only to learn the structure.
-This code is not perfect and can be complex.
-
-
-Additionally, remote is recommended for efficient performance management.
-Just know there is a way to do this.
-
-"""
-
-
-import logging
-
-import discord
-
-import discodo
-
-logging.basicConfig(level=logging.INFO)
-
-app = discord.Client()
-Audio = discodo.ClientManager()
-
-
-@app.event
-async def on_ready():
-    print("bot is now ready.")
-
-
-@app.event
-async def on_socket_response(payload):
-    Audio.discordDispatch(payload)
-
-
-@Audio.event("SOURCE_START")
-async def sendPlaying(VC, song):
-    await VC.channel.send(f'playing {song["title"]}')
-
-
-@Audio.event("SOURCE_END")
-async def sendStopped(VC, song):
-    await VC.channel.send(f'{song["title"]} Done')
-
-
-@app.event
-async def on_message(message):
-    await app.wait_until_ready()
-
-    if message.content.startswith("!join"):
-        if not message.author.voice:
-            return await message.channel.send("Join the voice channel first.")
-
-        await app.ws.voice_state(message.guild.id, message.author.voice.channel.id)
-        return await message.channel.send(
-            f"Connected to {message.author.voice.channel.mention}."
-        )
-
-    if message.content.startswith("!stop"):
-        vc = Audio.getVC(message.guild.id, safe=True)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        await vc.destroy()
-
-        return await message.channel.send("Player stopped and cleaned the queue.")
-
-    if message.content.startswith("!play"):
-        vc = Audio.getVC(message.guild.id, safe=True)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        if not hasattr(vc, "channel"):
-            vc.channel = message.channel
-
-        Source = await vc.loadSource(message.content[5:].strip())
-
-        if isinstance(Source, list):
-            return await message.channel.send(
-                f"{len(Source) - 1} songs except {Source[0].title} added."
-            )
-        else:
-            return await message.channel.send(f"{Source.title} added.")
-
-    if message.content.startswith("!skip"):
-        vc = Audio.getVC(message.guild.id, safe=True)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = int(message.content[5:].strip()) if message.content[5:].strip() else 1
-
-        vc.skip(offset)
-
-        return await message.channel.send(f"{offset} skipped.")
-
-    if message.content.startswith("!volume"):
-        vc = Audio.getVC(message.guild.id, safe=True)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = (
-            int(message.content[7:].strip()) if message.content[7:].strip() else 100
-        )
-
-        vc.volume = Volume = offset / 100
-
-        return await message.channel.send(f"Set volume to {Volume * 100}%.")
-
-    if message.content.startswith("!crossfade"):
-        vc = Audio.getVC(message.guild.id, safe=True)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = (
-            int(message.content[10:].strip()) if message.content[10:].strip() else 10
-        )
-
-        vc.crossfade = Crossfade = offset
-
-        return await message.channel.send(
-            f"Set crossfade seconds to {Crossfade} seconds."
-        )
-
-    if message.content.startswith("!autoplay"):
-        vc = Audio.getVC(message.guild.id, safe=True)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = (
-            int(message.content[9:].strip()) if message.content[9:].strip() else "on"
-        )
-        offset = {"on": True, "off": False}.get(offset, True)
-
-        vc.autoplay = autoplay = offset
-
-        return await message.channel.send(
-            f'Auto related play {"enabled" if autoplay else "disabled"}.'
-        )
-
-    if message.content.startswith("!np"):
-        vc = Audio.getVC(message.guild.id, safe=True)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        return await message.channel.send(
-            f"Now playing: {vc.player.current.title} `{vc.player.current.duration}:{vc.player.current.AudioData.duration}`"
-        )
-
-    if message.content.startswith("!shuffle"):
-        vc = Audio.getVC(message.guild.id, safe=True)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        vc.shuffle()
-
-        return await message.channel.send("Shuffle the queue.")
-
-    if message.content.startswith("!queue"):
-        vc = Audio.getVC(message.guild.id, safe=True)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        QueueText = "\n".join(
-            [str(vc.Queue.index(Item) + 1) + ". " + Item.title for Item in vc.Queue]
-        )
-
-        return await message.channel.send(
-            f"""
-Now playing: {vc.current.title} `{vc.current.position}:{vc.current.duration}`
-
-{QueueText}
-"""
-        )
-
-    if message.content.startswith("!seek"):
-        vc = Audio.getVC(message.guild.id, safe=True)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = int(message.content[5:].strip()) if message.content[5:].strip() else 1
-
-        await vc.seek(offset)
-
-        return await message.channel.send(f"Seek to {offset}.")
-
-
-app.run("SUPERRRSECRETTOKENNNNNN")
diff --git a/example/local/example2.py b/example/local/example2.py
deleted file mode 100644
index fbf2e967..00000000
--- a/example/local/example2.py
+++ /dev/null
@@ -1,175 +0,0 @@
-"""
-
-This example isn't recommended in production.
-Just use it only to learn the structure.
-This code is not perfect and can be complex.
-
-
-Additionally, remote is recommended for efficient performance management.
-Just know there is a way to do this.
-
-"""
-
-import logging
-
-import discord
-from discord.ext import commands
-from discord.ext.commands import Bot
-
-import discodo
-
-
-class MusicBot(commands.Cog):
-    def __init__(self, bot):
-        self.bot = bot
-        self.Audio = discodo.ClientManager()
-
-    @commands.Cog.listener()
-    async def on_socket_response(self, payload):
-        self.Audio.discordDispatch(payload)
-
-    @commands.Cog.listener()
-    async def on_ready(self):
-        print("bot is now ready.")
-
-    @commands.command(name="join")
-    async def _join(self, ctx):
-        if not ctx.author.voice:
-            return await ctx.send("Join the voice channel first.")
-
-        await self.bot.ws.voice_state(ctx.guild.id, ctx.author.voice.channel.id)
-        return await ctx.send(f"Connected to {ctx.author.voice.channel.id}.")
-
-    @commands.command(name="stop")
-    async def _stop(self, ctx):
-        vc = self.Audio.getVC(ctx.guild.id, safe=True)
-
-        if not vc:
-            return await ctx.send("Please type `!join` first.")
-
-        await vc.destroy()
-
-        return await ctx.send("Player stopped and cleaned the queue.")
-
-    @commands.command(name="play")
-    async def _play(self, ctx, *, music):
-        vc = self.Audio.getVC(ctx.guild.id, safe=True)
-
-        if not vc:
-            return await ctx.send("Please type `!join` first.")
-
-        Source = await vc.loadSource(music)
-
-        if isinstance(Source, list):
-            return await ctx.send(
-                f"{len(Source) - 1} songs except {Source[0].title} added."
-            )
-        else:
-            return await ctx.send(f"{Source.title} added.")
-
-    @commands.command(name="skip")
-    async def _skip(self, ctx, offset: int = 1):
-        vc = self.Audio.getVC(ctx.guild.id, safe=True)
-
-        if not vc:
-            return await ctx.send("Please type `!join` first.")
-
-        vc.skip(offset)
-
-        return await ctx.send(f"{offset} skipped.")
-
-    @commands.command(name="volume")
-    async def _volume(self, ctx, offset: int = 100):
-        vc = self.Audio.getVC(ctx.guild.id, safe=True)
-
-        if not vc:
-            return await ctx.send("Please type `!join` first.")
-
-        vc.volume = Volume = offset / 100
-
-        return await ctx.send(f"Set volume to {Volume * 100}%.")
-
-    @commands.command(name="crossfade")
-    async def _crossfade(self, ctx, offset: int = 10):
-        vc = self.Audio.getVC(ctx.guild.id, safe=True)
-
-        if not vc:
-            return await ctx.send("Please type `!join` first.")
-
-        vc.crossfade = Crossfade = offset
-
-        return await ctx.send(f"Set crossfade seconds to {Crossfade} seconds.")
-
-    @commands.command(name="autoplay")
-    async def _autoplay(self, ctx, offset: str = "on"):
-        vc = self.Audio.getVC(ctx.guild.id, safe=True)
-
-        if not vc:
-            return await ctx.send("Please type `!join` first.")
-
-        offset = {"on": True, "off": False}.get(offset, True)
-
-        vc.autoplay = autoplay = offset
-
-        return await ctx.send(
-            f'Auto related play {"enabled" if autoplay else "disabled"}.'
-        )
-
-    @commands.command(name="np")
-    async def _np(self, ctx):
-        vc = self.Audio.getVC(ctx.guild.id, safe=True)
-
-        if not vc:
-            return await ctx.send("Please type `!join` first.")
-
-        return await ctx.send(
-            f"Now playing: {vc.current.title} `{vc.current.position}:{vc.current.duration}`"
-        )
-
-    @commands.command(name="shuffle")
-    async def _shuffle(self, ctx):
-        vc = self.Audio.getVC(ctx.guild.id, safe=True)
-
-        if not vc:
-            return await ctx.send("Please type `!join` first.")
-
-        vc.shuffle()
-
-        return await ctx.send("Shuffle the queue.")
-
-    @commands.command(name="queue")
-    async def _queue(self, ctx):
-        vc = self.Audio.getVC(ctx.guild.id, safe=True)
-
-        if not vc:
-            return await ctx.send("Please type `!join` first.")
-
-        QueueText = "\n".join(
-            [str(vc.Queue.index(Item) + 1) + ". " + Item.title for Item in vc.Queue]
-        )
-
-        return await ctx.send(
-            f"""
-Now playing: {vc.current.title} `{vc.current.position}:{vc.player.current.duration}`
-
-{QueueText}
-"""
-        )
-
-    @commands.command(name="seek")
-    async def _seek(self, ctx, offset: int = 1):
-        vc = self.Audio.getVC(ctx.guild.id, safe=True)
-
-        if not vc:
-            return await ctx.send("Please type `!join` first.")
-
-        await vc.seek(offset)
-
-        return await ctx.send(f"Seek to {offset}.")
-
-
-if __name__ == "__main__":
-    logging.basicConfig(level=logging.INFO)
-    bot = Bot(command_prefix="!")
-    bot.add_cog(MusicBot(bot))
-    bot.run("SUPERRRSECRETTOKENNNNNN")
diff --git a/example/remote/example1.py b/example/remote/example1.py
deleted file mode 100644
index 76de6eb3..00000000
--- a/example/remote/example1.py
+++ /dev/null
@@ -1,238 +0,0 @@
-"""
-
-This example isn't recommended in production.
-Just use it only to learn the structure.
-This code is not perfect and can be complex.
-
-"""
-import logging
-import re
-
-import discord
-
-import discodo
-
-logging.basicConfig(level=logging.INFO)
-
-app = discord.Client()
-Audio = discodo.DPYClient(app)
-Audio.register_node("yourwonderfulnode", 8000, password="hellodiscodo")
-
-URL_REGEX = re.compile(
-    r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)"
-)
-
-
-@app.event
-async def on_ready():
-    print("bot is now ready.")
-
-
-@Audio.event("SOURCE_START")
-async def sendPlaying(VC, Data):
-    await VC.channel.send(f'playing {Data["source"]["title"]}')
-
-
-@Audio.event("SOURCE_END")
-async def sendStopped(VC, Data):
-    await VC.channel.send(f'{Data["source"]["title"]} Done')
-
-
-@app.event
-async def on_message(message):
-    await app.wait_until_ready()
-
-    if message.content.startswith("!join"):
-        if not message.author.voice:
-            return await message.channel.send("Join the voice channel first.")
-
-        await Audio.connect(message.author.voice.channel)
-        return await message.channel.send(
-            f"connected to {message.author.voice.channel.mention}.")
-
-    if message.content.startswith("!stop"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        await vc.destroy()
-
-        return await message.channel.send(
-            f"player stopped and cleaned the queue.")
-
-    if message.content.startswith("!play"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        if not hasattr(vc, "channel"):
-            vc.channel = message.channel
-
-        Source = await vc.loadSource(message.content[5:].strip())
-
-        if isinstance(Source, list):
-            return await message.channel.send(
-                f'{len(Source) - 1} songs except {Source[0]["title"]} added.')
-        else:
-            return await message.channel.send(
-                f'{Source["data"]["title"]} added.')
-
-    if message.content.startswith("!skip"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = int(
-            message.content[5:].strip()) if message.content[5:].strip() else 1
-
-        Remain = await vc.skip(offset)
-
-        return await message.channel.send(
-            f"{offset} skipped. {Remain}songs remain")
-
-    if message.content.startswith("!remove"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = int(
-            message.content[7:].strip()) if message.content[7:].strip() else 1
-
-        Data = await vc.remove(offset)
-
-        return await message.channel.send(
-            f'{Data["removed"]["title"]} removed.')
-
-    if message.content.startswith("!volume"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = (int(message.content[7:].strip())
-                  if message.content[7:].strip() else 100)
-
-        Volume = await vc.setVolume(offset / 100)
-
-        return await message.channel.send(f"set volume to {Volume}%.")
-
-    if message.content.startswith("!crossfade"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = (int(message.content[10:].strip())
-                  if message.content[10:].strip() else 10)
-
-        Crossfade = await vc.setCrossfade(offset)
-
-        return await message.channel.send(
-            f"set crossfade seconds to {Crossfade} seconds.")
-
-    if message.content.startswith("!autoplay"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = (int(message.content[9:].strip())
-                  if message.content[9:].strip() else "on")
-        offset = {"on": True, "off": False}.get(offset, True)
-
-        autoplay = await vc.setAutoplay(offset)
-
-        return await message.channel.send(
-            f'auto related play {"enabled" if autoplay else "disabled"}.')
-
-    if message.content.startswith("!np"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        Data = await vc.getState()
-
-        return await message.channel.send(
-            f'Now playing: {Data["current"]["title"]} `{Data["position"]}:{Data["duration"]}`'
-        )
-
-    if message.content.startswith("!shuffle"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        await vc.shuffle()
-
-        return await message.channel.send("shuffle the queue.")
-
-    if message.content.startswith("!queue"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        State = await vc.getState()
-        Queue = await vc.getQueue()
-        QueueText = "\n".join([
-            str(Queue.index(Item) + 1) + ". " + Item["title"] for Item in Queue
-        ])
-
-        return await message.channel.send(f"""
-Now playing: {State["current"]["title"]} `{State["position"]}:{State["duration"]}`
-
-{QueueText}
-""")
-
-    if message.content.startswith("!seek"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        offset = int(
-            message.content[5:].strip()) if message.content[5:].strip() else 1
-
-        await vc.seek(offset)
-
-        return await message.channel.send(f"seek to {offset}.")
-
-    if message.content.startswith("!subtitle"):
-        vc = Audio.getVC(message.guild)
-
-        if not vc:
-            return await message.channel.send("Please type `!join` first.")
-
-        language = message.content[9:].strip() if message.content[9:].strip(
-        ) else None
-        if not language:
-            return await message.channel.send("Please type language.")
-
-        if URL_REGEX.match(language):
-            url, language = language, None
-        else:
-            url = None
-
-        class SubtitleCallback:
-            def __init__(self):
-                self._msg = None
-
-            async def callback(self, subtitle):
-                if not self._msg or message.channel.last_message.id != self._msg.id:
-                    if self._msg:
-                        await self._msg.delete()
-                    self._msg = await message.channel.send(subtitle["current"])
-                else:
-                    await self._msg.edit(content=subtitle["current"])
-
-        Data = await vc.getSubtitle(lang=language,
-                                    url=url,
-                                    callback=SubtitleCallback().callback)
-
-
-app.run("SUPERRRSECRETTOKENNNNNN")
diff --git a/requirements-docs.txt b/requirements-docs.txt
new file mode 100644
index 00000000..5115359f
--- /dev/null
+++ b/requirements-docs.txt
@@ -0,0 +1,14 @@
+-r requirements.txt
+
+# Document generator
+Sphinx==3.4.3
+pygments
+sphinx-rtd-theme==0.5.1
+sphinxcontrib-httpdomain==1.7.0
+
+# Translation
+sphinx-intl==2.0.1
+transifex-client==0.14.2
+
+# For rtfd.io
+discord.py
diff --git a/requirements.txt b/requirements.txt
index 9ac18e54..0029a8b4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,14 +1,17 @@
 # For networking
-aiohttp>=3.6.0,<3.8.0
+aiohttp[speedups]>=3.6.0,<3.8.0
 websockets==8.1
 
+# For performance
+uvloop>=0.5.3; sys_platform != "win32" and implementation_name == "cpython"
+
 # Extra extractor resolver
 asyncspotify==0.12.1
 
 # Source extractor
 av==8.0.3
 PyNaCl==1.4.0
-youtube-dl==2021.4.17
+youtube-dl>=2021.4.7
 youtube-related==1.0.2
 
 # Subtitle parser
@@ -18,6 +21,10 @@ defusedxml==0.7.1
 markdownify==0.7.0
 
 # Logging
+rich==9.11.0
+
+# Node webserver
+sanic==21.3.4
 colorlog==5.0.1
 
 # Node webserver


Test patch:
diff --git a/test/test_resolver.py b/test/test_resolver.py
index aa35f9f6..f2b6b8c9 100644
--- a/test/test_resolver.py
+++ b/test/test_resolver.py
@@ -1,3 +1,4 @@
+import aiohttp
 import pytest
 
 from discodo.extractor.resolver import melon, vibe
@@ -5,28 +6,30 @@
 
 @pytest.mark.asyncio
 async def testMelon() -> None:
-    assert await melon.get_album(
-        "https://www.melon.com/album/detail.htm?albumId=10541761"
-    ) == ["쿠기 (Coogie) - POW (Feat. GRAY)"]
+    async with aiohttp.ClientSession() as session:
+        assert await melon.get_album(
+            "https://www.melon.com/album/detail.htm?albumId=10541761", session
+        ) == ["쿠기 (Coogie) - POW (Feat. GRAY)"]
 
 
 @pytest.mark.asyncio
 async def testVibe() -> None:
-    assert await vibe.getChart("total")
-
-    assert await vibe.getChart("genre-OS101")
-
-    assert await vibe.getTrack("1") == "강산에 - 거꾸로 강을 거슬러 오르는 저 힘찬 연어들처럼"
-
-    assert await vibe.getAlbum("1") == [
-        "강산에 - 거꾸로 강을 거슬러 오르는 저 힘찬 연어들처럼",
-        "강산에 - 미스템버린",
-        "강산에 - 코메디",
-        "강산에 - 억지",
-        "강산에 - 내마음의 구멍",
-        "강산에 - '98 아리랑",
-        "강산에 - 춤추는 나",
-        "강산에 - Cat Walk",
-        "강산에 - 맛!",
-        "강산에 - 나비의 입맞춤",
-    ]
+    async with aiohttp.ClientSession() as session:
+        assert await vibe.getChart("total", session)
+
+        assert await vibe.getChart("genre-OS101", session)
+
+        assert await vibe.getTrack("1", session) == "강산에 - 거꾸로 강을 거슬러 오르는 저 힘찬 연어들처럼"
+
+        assert await vibe.getAlbum("1", session) == [
+            "강산에 - 거꾸로 강을 거슬러 오르는 저 힘찬 연어들처럼",
+            "강산에 - 미스템버린",
+            "강산에 - 코메디",
+            "강산에 - 억지",
+            "강산에 - 내마음의 구멍",
+            "강산에 - '98 아리랑",
+            "강산에 - 춤추는 나",
+            "강산에 - Cat Walk",
+            "강산에 - 맛!",
+            "강산에 - 나비의 입맞춤",
+        ]
diff --git a/test/test_youtube.py b/test/test_youtube.py
index 00d7ee69..356288a3 100644
--- a/test/test_youtube.py
+++ b/test/test_youtube.py
@@ -1,3 +1,4 @@
+import aiohttp
 import pytest
 
 from discodo.extractor import youtube
@@ -5,25 +6,29 @@
 
 @pytest.mark.asyncio
 async def testSearch() -> None:
-    assert await youtube.search("T7Myg0nHGzg") == [
-        {
-            "id": "T7Myg0nHGzg",
-            "title": "CS:GO CLUCH CLIP (2vs1)",
-            "webpage_url": "https://www.youtube.com/watch?v=T7Myg0nHGzg",
-            "uploader": "라고솔로가말했습니다",
-            "duration": 19,
-        }
-    ]
+    async with aiohttp.ClientSession() as session:
+        assert await youtube.search("T7Myg0nHGzg", session) == [
+            {
+                "id": "T7Myg0nHGzg",
+                "title": "CS:GO CLUCH CLIP (2vs1)",
+                "webpage_url": "https://www.youtube.com/watch?v=T7Myg0nHGzg",
+                "uploader": "라고솔로가말했습니다",
+                "duration": 19,
+            }
+        ]
 
 
 @pytest.mark.asyncio
 async def testPlaylist() -> None:
-    assert await youtube.extract_playlist("PLB6rrfCPynfApD_C0yItgW5WLC0f-wDvG") == [
-        {
-            "id": "T7Myg0nHGzg",
-            "title": "CS:GO CLUCH CLIP (2vs1)",
-            "webpage_url": "https://youtube.com/watch?v=T7Myg0nHGzg",
-            "uploader": "라고솔로가말했습니다",
-            "duration": "19",
-        }
-    ]
+    async with aiohttp.ClientSession() as session:
+        assert await youtube.extract_playlist(
+            "PLB6rrfCPynfApD_C0yItgW5WLC0f-wDvG", session
+        ) == [
+            {
+                "id": "T7Myg0nHGzg",
+                "title": "CS:GO CLUCH CLIP (2vs1)",
+                "webpage_url": "https://youtube.com/watch?v=T7Myg0nHGzg",
+                "uploader": "라고솔로가말했습니다",
+                "duration": "19",
+            }
+        ]