music play + now playing
This commit is contained in:
parent
0749f742c3
commit
d09c2b7a89
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
|
.mypy_cache
|
||||||
__pycache__
|
__pycache__
|
||||||
config.toml
|
config.toml
|
||||||
download
|
download
|
||||||
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"python.formatting.provider": "black"
|
"python.formatting.provider": "black",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"python.analysis.typeCheckingMode": "off"
|
||||||
}
|
}
|
||||||
|
12
__main__.py
12
__main__.py
@ -5,9 +5,9 @@ from toml import TomlDecodeError
|
|||||||
|
|
||||||
from cog import Greetings, Music
|
from cog import Greetings, Music
|
||||||
from config import Config
|
from config import Config
|
||||||
from framework import Downloader, Redis, Spotify, Youtube
|
from framework import DiscordPlayer, Downloader, Redis, Spotify, Youtube
|
||||||
from logger import Logger
|
from logger import Logger
|
||||||
from service import FileManager, QueueManager
|
from service import FileManager, PlaybackManager, QueueManager
|
||||||
from usecase import Sources
|
from usecase import Sources
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@ -30,6 +30,12 @@ if __name__ == "__main__":
|
|||||||
# Redis Client
|
# Redis Client
|
||||||
redis = Redis(logger, config.redis, config.appName)
|
redis = Redis(logger, config.redis, config.appName)
|
||||||
|
|
||||||
|
# Discord Player
|
||||||
|
discordPlayer = DiscordPlayer()
|
||||||
|
|
||||||
|
# Playback Manager
|
||||||
|
playbackManager = PlaybackManager(discordPlayer)
|
||||||
|
|
||||||
# Queue Manager
|
# Queue Manager
|
||||||
queueManager = QueueManager(redis)
|
queueManager = QueueManager(redis)
|
||||||
|
|
||||||
@ -53,7 +59,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# Add Cogs
|
# Add Cogs
|
||||||
bot.add_cog(Greetings(bot, logger, redis))
|
bot.add_cog(Greetings(bot, logger, redis))
|
||||||
bot.add_cog(Music(bot, logger, queueManager, sources))
|
bot.add_cog(Music(bot, logger, queueManager, playbackManager, sources))
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
bot.run(config.discord.token)
|
bot.run(config.discord.token)
|
||||||
|
@ -15,7 +15,7 @@ class Greetings(Cog):
|
|||||||
@default_permissions(administrator=True)
|
@default_permissions(administrator=True)
|
||||||
async def redis_set(self, context: ApplicationContext, key: str, value: str):
|
async def redis_set(self, context: ApplicationContext, key: str, value: str):
|
||||||
self.logger.info(f"redis set {value} at {key}")
|
self.logger.info(f"redis set {value} at {key}")
|
||||||
await self.redis.set(key, value)
|
await self.redis.set("test", key, value)
|
||||||
|
|
||||||
await context.respond(f"redis set {value} at {key}")
|
await context.respond(f"redis set {value} at {key}")
|
||||||
|
|
||||||
@ -23,6 +23,6 @@ class Greetings(Cog):
|
|||||||
@default_permissions(administrator=True)
|
@default_permissions(administrator=True)
|
||||||
async def redis_get(self, context: ApplicationContext, key: str):
|
async def redis_get(self, context: ApplicationContext, key: str):
|
||||||
self.logger.info(f"redis get {key}")
|
self.logger.info(f"redis get {key}")
|
||||||
value = await self.redis.get(key)
|
value = await self.redis.get("test", key)
|
||||||
|
|
||||||
await context.respond(f"redis get {key}: {value}")
|
await context.respond(f"redis get {key}: {value}")
|
||||||
|
82
cog/music.py
82
cog/music.py
@ -1,25 +1,91 @@
|
|||||||
from logging import Logger
|
from logging import Logger
|
||||||
|
|
||||||
from discord import ApplicationContext, Bot, Cog, slash_command
|
from discord import ApplicationContext, Bot, Cog, Embed, Interaction, slash_command
|
||||||
|
|
||||||
from service import QueueManager
|
from service import PlaybackManager, QueueManager
|
||||||
from usecase import Sources
|
from usecase import Sources
|
||||||
|
|
||||||
|
|
||||||
class Music(Cog):
|
class Music(Cog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, bot: Bot, logger: Logger, queueManager: QueueManager, sources: Sources
|
self,
|
||||||
|
bot: Bot,
|
||||||
|
logger: Logger,
|
||||||
|
queueManager: QueueManager,
|
||||||
|
playbackManager: PlaybackManager,
|
||||||
|
sources: Sources,
|
||||||
):
|
):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.queueManager = queueManager
|
self.queueManager = queueManager
|
||||||
|
self.playbackManager = playbackManager
|
||||||
self.sources = sources
|
self.sources = sources
|
||||||
|
|
||||||
@slash_command(name="play")
|
@slash_command(name="play")
|
||||||
async def play(self, context: ApplicationContext, query: str):
|
async def play(self, context: ApplicationContext, query: str):
|
||||||
async with self.queueManager(context.guild_id) as queue:
|
if context.author.voice is None:
|
||||||
interaction = await context.respond(f"searching {query} ...")
|
await context.respond("Not connected to a voice channel")
|
||||||
entries = await self.sources.processQuery(interaction, query)
|
return
|
||||||
queue.add(entries)
|
|
||||||
|
|
||||||
await interaction.edit_original_response(content=entries[0].title.name)
|
async with self.queueManager(context.guild_id) as queue:
|
||||||
|
interaction = await context.respond(f"Searching {query}...")
|
||||||
|
if not isinstance(interaction, Interaction):
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = await self.sources.processQuery(interaction, query)
|
||||||
|
|
||||||
|
if entries is None:
|
||||||
|
await interaction.edit_original_response(content=f"{query} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
queue.add(entries)
|
||||||
|
await self.playbackManager.registerQueue(
|
||||||
|
context.guild_id,
|
||||||
|
queue,
|
||||||
|
context.channel,
|
||||||
|
context.author.voice.channel,
|
||||||
|
)
|
||||||
|
|
||||||
|
if entries[0].playlist is not None:
|
||||||
|
await interaction.edit_original_response(
|
||||||
|
content=f"{len(entries)} songs added to queue from playlist {entries[0].playlist.name}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.edit_original_response(
|
||||||
|
content=f"{entries[0].title.name} was added to queue"
|
||||||
|
)
|
||||||
|
|
||||||
|
@slash_command(name="nowplaying")
|
||||||
|
async def nowPlaying(self, context: ApplicationContext):
|
||||||
|
assert self.bot.user
|
||||||
|
async with self.queueManager(context.guild_id) as queue:
|
||||||
|
entry = queue.nowPlaying()
|
||||||
|
|
||||||
|
if entry is None:
|
||||||
|
await context.respond("No song playing")
|
||||||
|
return
|
||||||
|
|
||||||
|
requester = await self.bot.get_or_fetch_user(entry.requester)
|
||||||
|
assert requester is not None
|
||||||
|
|
||||||
|
embed = (
|
||||||
|
Embed(title=entry.title.name, url=entry.title.url)
|
||||||
|
.add_field(
|
||||||
|
name="Artist", value=f"[{entry.artist.name}]({entry.artist.url})"
|
||||||
|
)
|
||||||
|
.set_author(
|
||||||
|
name="Now Playing", icon_url=self.bot.user.display_avatar.url
|
||||||
|
)
|
||||||
|
.set_image(url=entry.thumbnail)
|
||||||
|
.set_footer(
|
||||||
|
text=requester.display_name, icon_url=requester.display_avatar.url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if entry.playlist is not None:
|
||||||
|
embed.add_field(
|
||||||
|
name="Playlist",
|
||||||
|
value=f"[{entry.playlist.name}]({entry.playlist.url})",
|
||||||
|
)
|
||||||
|
|
||||||
|
await context.respond(embed=embed)
|
||||||
|
52
config.py
52
config.py
@ -2,46 +2,46 @@ import toml
|
|||||||
|
|
||||||
|
|
||||||
class DiscordConfig:
|
class DiscordConfig:
|
||||||
def __init__(self, discord_config) -> None:
|
def __init__(self, discordConfig) -> None:
|
||||||
self.token: str = discord_config["token"]
|
self.token: str = discordConfig["token"]
|
||||||
self.timeout: int = discord_config["timeout"]
|
self.timeout: int = discordConfig["timeout"]
|
||||||
|
|
||||||
|
|
||||||
class YoutubeConfig:
|
class YoutubeConfig:
|
||||||
def __init__(self, youtube_config) -> None:
|
def __init__(self, youtubeConfig) -> None:
|
||||||
self.language: str = youtube_config["language"]
|
self.language: str = youtubeConfig["language"]
|
||||||
self.region: str = youtube_config["region"]
|
self.region: str = youtubeConfig["region"]
|
||||||
|
|
||||||
|
|
||||||
class SpotifyConfig:
|
class SpotifyConfig:
|
||||||
def __init__(self, spotify_config) -> None:
|
def __init__(self, spotifyConfig) -> None:
|
||||||
self.clientId: str = spotify_config["clientId"]
|
self.clientId: str = spotifyConfig["clientId"]
|
||||||
self.clientSecret: str = spotify_config["clientSecret"]
|
self.clientSecret: str = spotifyConfig["clientSecret"]
|
||||||
self.region: str = spotify_config["region"]
|
self.region: str = spotifyConfig["region"]
|
||||||
|
|
||||||
|
|
||||||
class LoggingConfig:
|
class LoggingConfig:
|
||||||
def __init__(self, logging_config) -> None:
|
def __init__(self, loggingConfig) -> None:
|
||||||
self.level: str = logging_config["level"]
|
self.level: str = loggingConfig["level"]
|
||||||
|
|
||||||
|
|
||||||
class RedisConfig:
|
class RedisConfig:
|
||||||
def __init__(self, redis_config) -> None:
|
def __init__(self, redisConfig) -> None:
|
||||||
self.host: str = redis_config["host"]
|
self.host: str = redisConfig["host"]
|
||||||
self.port: int = redis_config["port"]
|
self.port: int = redisConfig["port"]
|
||||||
self.password: str = redis_config["password"]
|
self.password: str = redisConfig["password"]
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
def __init__(self, config_path: str) -> None:
|
def __init__(self, configPath: str) -> None:
|
||||||
self._config = toml.load(config_path)
|
self.config = toml.load(configPath)
|
||||||
self.appName: str = self._config["appName"]
|
self.appName: str = self.config["appName"]
|
||||||
self.downloadDirectory: str = self._config["downloadDirectory"]
|
self.downloadDirectory: str = self.config["downloadDirectory"]
|
||||||
self.discord = DiscordConfig(self._config["discord"])
|
self.discord = DiscordConfig(self.config["discord"])
|
||||||
self.youtube = YoutubeConfig(self._config["youtube"])
|
self.youtube = YoutubeConfig(self.config["youtube"])
|
||||||
self.spotify = SpotifyConfig(self._config["spotify"])
|
self.spotify = SpotifyConfig(self.config["spotify"])
|
||||||
self.logging = LoggingConfig(self._config["logging"])
|
self.logging = LoggingConfig(self.config["logging"])
|
||||||
self.redis = RedisConfig(self._config["redis"])
|
self.redis = RedisConfig(self.config["redis"])
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self._config)
|
return str(self.config)
|
||||||
|
@ -3,37 +3,60 @@ from .entry import Entry
|
|||||||
|
|
||||||
class Queue:
|
class Queue:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._entries: list[Entry] = []
|
self.entries: list[Entry] = []
|
||||||
|
self.playing = False
|
||||||
self.cursor = 0
|
self.cursor = 0
|
||||||
|
self.seek = 0
|
||||||
|
|
||||||
def add(self, entries: list[Entry]) -> None:
|
def add(self, entries: list[Entry]) -> None:
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
self._entries.append(entry)
|
self.entries.append(entry)
|
||||||
|
|
||||||
def remove(self, index: int, recursive: bool) -> None:
|
def remove(self, index: int, recursive: bool) -> None:
|
||||||
if not 0 < index < len(self._entries):
|
if not 0 <= index < len(self.entries):
|
||||||
return
|
return
|
||||||
|
|
||||||
# if recursive and self[index].playlist is not None:
|
# if recursive and self[index].playlist is not None:
|
||||||
# first_entry = ""
|
# firstEntry = ""
|
||||||
# else:
|
# else:
|
||||||
self._entries.pop()
|
self.entries.pop()
|
||||||
|
|
||||||
def move(self, frm: int, to: int) -> None:
|
def move(self, frm: int, to: int) -> None:
|
||||||
if (
|
if (
|
||||||
not 0 < frm < len(self._entries)
|
not 0 <= frm < len(self.entries)
|
||||||
or not 0 < to < len(self._entries)
|
or not 0 <= to < len(self.entries)
|
||||||
or frm == to
|
or frm == to
|
||||||
):
|
):
|
||||||
return
|
return None
|
||||||
|
|
||||||
self._entries.insert(to, self._entries.pop(frm))
|
self.entries.insert(to, self.entries.pop(frm))
|
||||||
|
|
||||||
|
def moveCursor(self, position: int) -> None:
|
||||||
|
if not 0 <= position <= len(self.entries):
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.cursor = position
|
||||||
|
|
||||||
|
def incrementCursor(self) -> None:
|
||||||
|
self.moveCursor(self.cursor + 1)
|
||||||
|
|
||||||
|
def nowPlaying(self) -> Entry | None:
|
||||||
|
if not 0 <= self.cursor < len(self.entries):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.entries[self.cursor]
|
||||||
|
|
||||||
|
def startPlaying(self) -> None:
|
||||||
|
self.playing = True
|
||||||
|
|
||||||
|
def stopPlaying(self) -> None:
|
||||||
|
self.playing = False
|
||||||
|
|
||||||
def __getitem__(self, index: int) -> Entry | None:
|
def __getitem__(self, index: int) -> Entry | None:
|
||||||
if not 0 < index < len(self._entries):
|
if not 0 <= index < len(self.entries):
|
||||||
return
|
return None
|
||||||
|
|
||||||
return self._entries[index]
|
return self.entries[index]
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return len(self._entries)
|
return len(self.entries)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from .discordPlayer import DiscordPlayer
|
||||||
from .downloader import Downloader
|
from .downloader import Downloader
|
||||||
from .redis import Redis
|
from .redis import Redis
|
||||||
from .spotify import Spotify
|
from .spotify import Spotify
|
||||||
|
40
framework/discordPlayer.py
Normal file
40
framework/discordPlayer.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from discord import FFmpegOpusAudio, VoiceChannel, VoiceClient
|
||||||
|
|
||||||
|
from entity import File
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordPlayer:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.clients: dict[int, VoiceClient] = {}
|
||||||
|
self.timeout = 60
|
||||||
|
|
||||||
|
async def connect(self, guildId: int, voiceChannel: VoiceChannel) -> None:
|
||||||
|
if (
|
||||||
|
guildId not in self.clients
|
||||||
|
or self.clients[guildId] is None
|
||||||
|
or not self.clients[guildId].is_connected()
|
||||||
|
):
|
||||||
|
self.clients[guildId] = await voiceChannel.connect(timeout=self.timeout)
|
||||||
|
print(f"connect to {voiceChannel.name}")
|
||||||
|
|
||||||
|
async def disconnect(self, guildId: int) -> None:
|
||||||
|
if guildId in self.clients and self.clients[guildId].is_connected():
|
||||||
|
await self.clients[guildId].disconnect()
|
||||||
|
|
||||||
|
def play(
|
||||||
|
self, guildId: int, file: File | str, seekTime: int = 0, after=None
|
||||||
|
) -> None:
|
||||||
|
self.clients[guildId].play(
|
||||||
|
FFmpegOpusAudio(
|
||||||
|
file.name if isinstance(file, File) else file,
|
||||||
|
bitrate=256,
|
||||||
|
before_options="-ss %d" % seekTime,
|
||||||
|
options="-vn",
|
||||||
|
),
|
||||||
|
after=lambda error: self.clients[guildId].loop.create_task(after(error))
|
||||||
|
if after is not None
|
||||||
|
else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self, guildId) -> None:
|
||||||
|
self.clients[guildId].stop()
|
@ -1,47 +1,34 @@
|
|||||||
import pickle
|
import pickle
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
|
|
||||||
import redis.asyncio as redis
|
from redis.asyncio import Redis as RedisClient
|
||||||
from redis.asyncio.lock import Lock
|
|
||||||
|
|
||||||
from config import RedisConfig
|
from config import RedisConfig
|
||||||
|
|
||||||
|
|
||||||
class Redis:
|
class Redis:
|
||||||
def __init__(self, logger: Logger, config: RedisConfig, rootKeyName: str) -> None:
|
def __init__(self, logger: Logger, config: RedisConfig, rootKeyName: str) -> None:
|
||||||
self._client = redis.Redis(
|
self.client: RedisClient = RedisClient(
|
||||||
host=config.host,
|
host=config.host,
|
||||||
port=config.port,
|
port=config.port,
|
||||||
password=config.password,
|
password=config.password,
|
||||||
auto_close_connection_pool=False,
|
auto_close_connection_pool=False,
|
||||||
)
|
)
|
||||||
self._locks: dict[str, Lock] = {}
|
|
||||||
self.rootKeyName = rootKeyName
|
self.rootKeyName = rootKeyName
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
async def _get_lock(self, key) -> Lock:
|
async def get(self, context: str, key: str):
|
||||||
if key not in self._locks:
|
|
||||||
self._locks[key] = self._client.lock(key)
|
|
||||||
return self._locks[key]
|
|
||||||
|
|
||||||
async def acquire(self, key: str) -> None:
|
|
||||||
lock = await self._get_lock(f"{self.rootKeyName}:queue:{key}")
|
|
||||||
await lock.acquire()
|
|
||||||
|
|
||||||
async def release(self, key: str) -> None:
|
|
||||||
lock = await self._get_lock(f"{self.rootKeyName}:queue:{key}")
|
|
||||||
await lock.release()
|
|
||||||
|
|
||||||
async def get(self, key: str):
|
|
||||||
self.logger.info(f"get value {key} from redis")
|
self.logger.info(f"get value {key} from redis")
|
||||||
value = await self._client.get(f"{self.rootKeyName}:queue:{key}")
|
value = await self.client.get(f"{self.rootKeyName}:{context}:{key}")
|
||||||
if value:
|
if value:
|
||||||
return pickle.loads(value)
|
return pickle.loads(value)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def set(self, key: str, value) -> None:
|
async def set(self, context: str, key: str, value) -> None:
|
||||||
self.logger.info(f"set value {key} to redis")
|
self.logger.info(f"set value {key} to redis")
|
||||||
await self._client.set(f"{self.rootKeyName}:queue:{key}", pickle.dumps(value))
|
await self.client.set(
|
||||||
|
f"{self.rootKeyName}:{context}:{key}", pickle.dumps(value)
|
||||||
|
)
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
await self._client.close()
|
await self.client.close()
|
||||||
|
@ -21,5 +21,10 @@ class Spotify:
|
|||||||
|
|
||||||
async def getTrack(self, url: str):
|
async def getTrack(self, url: str):
|
||||||
return await self.loop.run_in_executor(
|
return await self.loop.run_in_executor(
|
||||||
None, lambda: self.client.track(url, self.region)
|
None, lambda: self.client.track(url, market=self.region)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def getPlaylist(self, url: str):
|
||||||
|
return await self.loop.run_in_executor(
|
||||||
|
None, lambda: self.client.playlist(url, market=self.region)
|
||||||
)
|
)
|
||||||
|
@ -17,6 +17,7 @@ class Youtube:
|
|||||||
self.params = {
|
self.params = {
|
||||||
"format": "bestaudio/best",
|
"format": "bestaudio/best",
|
||||||
"outtmpl": path.join(downloadDirectory, "%(id)s"),
|
"outtmpl": path.join(downloadDirectory, "%(id)s"),
|
||||||
|
"updatetime": False,
|
||||||
"restrictfilenames": True,
|
"restrictfilenames": True,
|
||||||
"noplaylist": True,
|
"noplaylist": True,
|
||||||
"ignoreerrors": True,
|
"ignoreerrors": True,
|
||||||
@ -37,7 +38,7 @@ class Youtube:
|
|||||||
videosSearch = VideosSearch(
|
videosSearch = VideosSearch(
|
||||||
query, limit=1, language=self.language, region=self.region
|
query, limit=1, language=self.language, region=self.region
|
||||||
)
|
)
|
||||||
return await videosSearch.next()
|
return (await videosSearch.next())["result"]
|
||||||
|
|
||||||
async def fetchData(self, url: str):
|
async def fetchData(self, url: str):
|
||||||
info = await self.loop.run_in_executor(
|
info = await self.loop.run_in_executor(
|
||||||
@ -66,7 +67,7 @@ class Youtube:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def download(self, url: str, fileName: str, progress=None) -> None:
|
async def download(self, url: str, fileName: str, progress=None) -> File:
|
||||||
if progress is not None:
|
if progress is not None:
|
||||||
progress_hook = lambda status: self.downloadProgress(progress, status)
|
progress_hook = lambda status: self.downloadProgress(progress, status)
|
||||||
self.client.add_progress_hook(progress_hook)
|
self.client.add_progress_hook(progress_hook)
|
||||||
|
2
lint.sh
Executable file
2
lint.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
isort . && black . && flake8 . && mypy .
|
8
requirements-dev.txt
Normal file
8
requirements-dev.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
types-redis==4.5.5.0
|
||||||
|
types-requests==2.30.0.0
|
||||||
|
types-toml==0.10.8.6
|
||||||
|
|
||||||
|
black==23.3.0
|
||||||
|
flake8==6.0.0
|
||||||
|
isort==5.12.0
|
||||||
|
mypy==1.2.0
|
@ -5,7 +5,3 @@ spotipy==2.23.0
|
|||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
youtube-search-python==1.6.6
|
youtube-search-python==1.6.6
|
||||||
yt-dlp==2023.3.4
|
yt-dlp==2023.3.4
|
||||||
|
|
||||||
black==23.3.0
|
|
||||||
flake8==6.0.0
|
|
||||||
isort==5.12.0
|
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
from .fileManager import FileManager
|
from .fileManager import FileManager
|
||||||
|
from .playbackManager import PlaybackManager
|
||||||
from .queueManager import QueueManager
|
from .queueManager import QueueManager
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import re
|
import re
|
||||||
from os import path
|
from collections.abc import AsyncIterator
|
||||||
|
from os import path, stat
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from discord import Interaction
|
from discord import Interaction
|
||||||
|
|
||||||
from entity import Entry
|
from entity import Entry, File
|
||||||
from framework import Downloader, Youtube
|
from framework import Downloader, Youtube
|
||||||
|
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ class FileManager:
|
|||||||
self.youtubeVideoRegex = re.compile(
|
self.youtubeVideoRegex = re.compile(
|
||||||
r"(https:\/\/)?(www|music)\.youtube\.com\/(watch\?v=|shorts\/)\w*"
|
r"(https:\/\/)?(www|music)\.youtube\.com\/(watch\?v=|shorts\/)\w*"
|
||||||
)
|
)
|
||||||
self.progressLastUpdate = 0
|
self.progressLastUpdate = 0.0
|
||||||
|
|
||||||
def getFileName(self, entryId: str) -> str:
|
def getFileName(self, entryId: str) -> str:
|
||||||
return path.join(self.downloadDirectory, entryId)
|
return path.join(self.downloadDirectory, entryId)
|
||||||
@ -27,24 +28,29 @@ class FileManager:
|
|||||||
self,
|
self,
|
||||||
interaction: Interaction,
|
interaction: Interaction,
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
current_size: int,
|
current_size: float,
|
||||||
total_size: int,
|
total_size: float,
|
||||||
):
|
):
|
||||||
if time() - self.progressLastUpdate > 1:
|
current_size = current_size / 1024 / 1024
|
||||||
|
total_size = total_size / 1024 / 1024
|
||||||
|
if time() - self.progressLastUpdate > 1 and total_size > 10:
|
||||||
self.progressLastUpdate = time()
|
self.progressLastUpdate = time()
|
||||||
await interaction.edit_original_response(
|
await interaction.edit_original_response(
|
||||||
content=f"Downloading {entry.title.name} [%.2f/%.2f Mo]"
|
content=f"Downloading {entry.title.name} [%.2f/%.2f Mo]"
|
||||||
% (current_size / 1024 / 1024, total_size / 1024 / 1024)
|
% (current_size, total_size)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def download(self, interaction: Interaction, entries: list[Entry]):
|
async def download(
|
||||||
|
self, interaction: Interaction, entries: list[Entry]
|
||||||
|
) -> AsyncIterator[File]:
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
entryId = self.youtube.getId(entry.title.url)
|
print("DOWNLOAD", entry.source)
|
||||||
|
entryId = self.youtube.getId(entry.source)
|
||||||
|
|
||||||
fileName = self.getFileName(entryId)
|
fileName = self.getFileName(entryId)
|
||||||
if not path.isfile(fileName) and isinstance(entry.source, str):
|
if not path.isfile(fileName) and isinstance(entry.source, str):
|
||||||
if self.youtubeVideoRegex.match(entry.source) is not None:
|
if self.youtubeVideoRegex.match(entry.source) is not None:
|
||||||
entry.source = await self.youtube.download(
|
yield await self.youtube.download(
|
||||||
entry.source,
|
entry.source,
|
||||||
fileName,
|
fileName,
|
||||||
lambda current_size, total_size: self.downloadProgress(
|
lambda current_size, total_size: self.downloadProgress(
|
||||||
@ -52,10 +58,12 @@ class FileManager:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
entry.source = await self.downloader.download(
|
yield await self.downloader.download(
|
||||||
entry.source,
|
entry.source,
|
||||||
fileName,
|
fileName,
|
||||||
lambda current_size, total_size: self.downloadProgress(
|
lambda current_size, total_size: self.downloadProgress(
|
||||||
interaction, entry, current_size, total_size
|
interaction, entry, current_size, total_size
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
yield File(fileName, stat(fileName).st_size)
|
||||||
|
59
service/playbackManager.py
Normal file
59
service/playbackManager.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from discord import TextChannel, VoiceChannel
|
||||||
|
|
||||||
|
from entity import Queue
|
||||||
|
from framework import DiscordPlayer
|
||||||
|
|
||||||
|
|
||||||
|
class ActiveQueue:
|
||||||
|
def __init__(
|
||||||
|
self, queue: Queue, textChannel: TextChannel, voiceChannel: VoiceChannel
|
||||||
|
) -> None:
|
||||||
|
self.queue = queue
|
||||||
|
self.textChannel = textChannel
|
||||||
|
self.voiceChannel = voiceChannel
|
||||||
|
|
||||||
|
|
||||||
|
class PlaybackManager:
|
||||||
|
def __init__(self, player: DiscordPlayer) -> None:
|
||||||
|
self.player = player
|
||||||
|
self.queues: dict[int, ActiveQueue] = {}
|
||||||
|
|
||||||
|
async def nextTrack(self, error: Exception, guildId: int):
|
||||||
|
queue = self.queues[guildId].queue
|
||||||
|
textChannel = self.queues[guildId].textChannel
|
||||||
|
queue.incrementCursor()
|
||||||
|
if queue.cursor < len(queue):
|
||||||
|
self.startPlayback(guildId)
|
||||||
|
await textChannel.send(f"Playing {queue[queue.cursor].title.name}")
|
||||||
|
else:
|
||||||
|
self.stopPlayback(guildId)
|
||||||
|
|
||||||
|
def startPlayback(self, guildId: int, seekTime=0):
|
||||||
|
queue = self.queues[guildId].queue
|
||||||
|
if len(queue) > queue.cursor and not queue.playing:
|
||||||
|
queue.startPlaying()
|
||||||
|
self.player.play(
|
||||||
|
guildId,
|
||||||
|
queue[queue.cursor].source,
|
||||||
|
seekTime,
|
||||||
|
lambda error: self.nextTrack(error, guildId),
|
||||||
|
)
|
||||||
|
|
||||||
|
def stopPlayback(self, guildId: int):
|
||||||
|
queue = self.queues[guildId].queue
|
||||||
|
if queue.playing:
|
||||||
|
queue.stopPlaying()
|
||||||
|
self.player.stop(guildId)
|
||||||
|
|
||||||
|
async def registerQueue(
|
||||||
|
self,
|
||||||
|
guildId: int,
|
||||||
|
queue: Queue,
|
||||||
|
textChannel: TextChannel,
|
||||||
|
voiceChannel: VoiceChannel,
|
||||||
|
):
|
||||||
|
if guildId not in self.queues:
|
||||||
|
self.queues[guildId] = ActiveQueue(queue, textChannel, voiceChannel)
|
||||||
|
|
||||||
|
await self.player.connect(guildId, voiceChannel)
|
||||||
|
self.startPlayback(guildId)
|
@ -1,3 +1,4 @@
|
|||||||
|
from collections.abc import AsyncIterator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from entity import Queue
|
from entity import Queue
|
||||||
@ -10,21 +11,13 @@ class QueueManager:
|
|||||||
self.queues: dict[int, Queue] = {}
|
self.queues: dict[int, Queue] = {}
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def __call__(self, guildId: int) -> Queue:
|
async def __call__(self, guildId: int) -> AsyncIterator[Queue]:
|
||||||
# await self.acquire(guildId)
|
|
||||||
queue = await self.get(guildId)
|
queue = await self.get(guildId)
|
||||||
yield queue
|
yield queue
|
||||||
await self.save(guildId)
|
await self.save(guildId)
|
||||||
# await self.release(guildId)
|
|
||||||
|
|
||||||
async def acquire(self, guildId: int) -> None:
|
|
||||||
await self.redis.acquire(guildId)
|
|
||||||
|
|
||||||
async def release(self, guildId: int) -> None:
|
|
||||||
await self.redis.release(guildId)
|
|
||||||
|
|
||||||
async def get(self, guildId: int) -> Queue:
|
async def get(self, guildId: int) -> Queue:
|
||||||
queue: Queue | None = await self.redis.get(guildId)
|
queue: Queue | None = await self.redis.get("queue", str(guildId))
|
||||||
if queue is None:
|
if queue is None:
|
||||||
queue = Queue()
|
queue = Queue()
|
||||||
self.queues[guildId] = queue
|
self.queues[guildId] = queue
|
||||||
@ -32,4 +25,4 @@ class QueueManager:
|
|||||||
return self.queues[guildId]
|
return self.queues[guildId]
|
||||||
|
|
||||||
async def save(self, guildId: int) -> None:
|
async def save(self, guildId: int) -> None:
|
||||||
await self.redis.set(guildId, self.queues[guildId])
|
await self.redis.set("queue", str(guildId), self.queues[guildId])
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from discord import Interaction
|
from discord import Interaction
|
||||||
@ -50,8 +49,8 @@ class Sources:
|
|||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with open("search_result.json", "w") as file:
|
# with open("search_result.json", "w") as file:
|
||||||
file.write(json.dumps(data))
|
# file.write(json.dumps(data))
|
||||||
|
|
||||||
entry = Entry(
|
entry = Entry(
|
||||||
title=Title(name=data["title"], url=data["webpage_url"]),
|
title=Title(name=data["title"], url=data["webpage_url"]),
|
||||||
@ -62,48 +61,56 @@ class Sources:
|
|||||||
source=data["webpage_url"],
|
source=data["webpage_url"],
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.fileManager.download(interaction, [entry])
|
entry.source = await anext(self.fileManager.download(interaction, [entry]))
|
||||||
|
|
||||||
return [entry]
|
return [entry]
|
||||||
|
|
||||||
if self.youtube_playlist_regex.match(query) is not None:
|
if self.youtube_playlist_regex.match(query) is not None:
|
||||||
data = await self.youtube.getData(query)
|
data = await self.youtube.fetchData(query)
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with open("search_result.json", "w") as file:
|
# with open("search_result.json", "w") as file:
|
||||||
file.write(json.dumps(data))
|
# file.write(json.dumps(data))
|
||||||
|
|
||||||
playlist = Playlist(
|
playlist = Playlist(
|
||||||
title=data["title"],
|
name=data["title"],
|
||||||
url=data["webpage_url"],
|
url=data["webpage_url"],
|
||||||
owner=Artist(name=data["channel"], url=data["channel_url"]),
|
owner=Artist(name=data["channel"], url=data["channel_url"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
entries = [
|
||||||
Entry(
|
Entry(
|
||||||
title=Title(name=data["title"], url=data["webpage_url"]),
|
title=Title(
|
||||||
artist=Artist(name=data["channel"], url=data["channel_url"]),
|
name=entry_data["title"], url=entry_data["webpage_url"]
|
||||||
duration=data["duration"],
|
),
|
||||||
thumbnail=data["thumbnail"],
|
artist=Artist(
|
||||||
|
name=entry_data["channel"], url=entry_data["channel_url"]
|
||||||
|
),
|
||||||
|
duration=entry_data["duration"],
|
||||||
|
thumbnail=entry_data["thumbnail"],
|
||||||
requesterId=interaction.user.id,
|
requesterId=interaction.user.id,
|
||||||
playlist=playlist,
|
playlist=playlist,
|
||||||
source=data["url"],
|
source=entry_data["webpage_url"],
|
||||||
)
|
)
|
||||||
for data in data["entries"]
|
for entry_data in data["entries"]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
async for file in self.fileManager.download(interaction, entries):
|
||||||
|
entries[index].source = file
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
if self.spotify_track_regex.match(query) is not None:
|
if self.spotify_track_regex.match(query) is not None:
|
||||||
data = await self.spotify.getTrack(query)
|
data = await self.spotify.getTrack(query)
|
||||||
|
|
||||||
with open("spotify_track_result.json", "w") as file:
|
# with open("spotify_track_result.json", "w") as file:
|
||||||
file.write(json.dumps(data))
|
# file.write(json.dumps(data))
|
||||||
|
|
||||||
return [
|
entry = Entry(
|
||||||
Entry(
|
title=Title(name=data["name"], url=data["external_urls"]["spotify"]),
|
||||||
title=Title(
|
|
||||||
name=data["name"], url=data["external_urls"]["spotify"]
|
|
||||||
),
|
|
||||||
artist=Artist(
|
artist=Artist(
|
||||||
name=data["artists"][0]["name"],
|
name=data["artists"][0]["name"],
|
||||||
url=data["artists"][0]["external_urls"]["spotify"],
|
url=data["artists"][0]["external_urls"]["spotify"],
|
||||||
@ -116,17 +123,76 @@ class Sources:
|
|||||||
thumbnail=data["album"]["images"][0]["url"],
|
thumbnail=data["album"]["images"][0]["url"],
|
||||||
requesterId=interaction.user.id,
|
requesterId=interaction.user.id,
|
||||||
)
|
)
|
||||||
]
|
|
||||||
|
entry.source = await self.search(f"{entry.title.name} {entry.artist.name}")
|
||||||
|
entry.source = await anext(self.fileManager.download(interaction, [entry]))
|
||||||
|
|
||||||
|
return [entry]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.spotify_playlist_regex.match(query) is not None
|
self.spotify_playlist_regex.match(query) is not None
|
||||||
or self.spotify_album_regex.match(query) is not None
|
or self.spotify_album_regex.match(query) is not None
|
||||||
or self.spotify_artist_regex.match(query) is not None
|
or self.spotify_artist_regex.match(query) is not None
|
||||||
):
|
):
|
||||||
return
|
data = await self.spotify.getPlaylist(query)
|
||||||
|
|
||||||
return await self.processQuery(interaction, await self.search(query))
|
# with open("spotify_playlist_result.json", "w") as file:
|
||||||
|
# file.write(json.dumps(data))
|
||||||
|
|
||||||
async def search(self, query) -> str:
|
playlist = Playlist(
|
||||||
|
name=data["name"],
|
||||||
|
url=data["external_urls"]["spotify"],
|
||||||
|
owner=Artist(
|
||||||
|
name=data["owner"]["display_name"],
|
||||||
|
url=data["owner"]["external_urls"]["spotify"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
entries = [
|
||||||
|
Entry(
|
||||||
|
title=Title(
|
||||||
|
name=entry_data["track"]["name"],
|
||||||
|
url=entry_data["track"]["external_urls"]["spotify"],
|
||||||
|
),
|
||||||
|
artist=Artist(
|
||||||
|
name=entry_data["track"]["artists"][0]["name"],
|
||||||
|
url=entry_data["track"]["artists"][0]["external_urls"][
|
||||||
|
"spotify"
|
||||||
|
],
|
||||||
|
),
|
||||||
|
album=Album(
|
||||||
|
name=entry_data["track"]["album"]["name"],
|
||||||
|
url=entry_data["track"]["album"]["external_urls"]["spotify"],
|
||||||
|
),
|
||||||
|
duration=0,
|
||||||
|
thumbnail=entry_data["track"]["album"]["images"][0]["url"],
|
||||||
|
requesterId=interaction.user.id,
|
||||||
|
)
|
||||||
|
for entry_data in data["tracks"]["items"]
|
||||||
|
]
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
entry.source = await self.search(
|
||||||
|
f"{entry.title.name} {entry.artist.name}"
|
||||||
|
)
|
||||||
|
print(entry.source)
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
async for file in self.fileManager.download(interaction, entries):
|
||||||
|
print(entries[index].source)
|
||||||
|
entries[index].source = file
|
||||||
|
print(entries[index].source)
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
searchResult = await self.search(query)
|
||||||
|
if searchResult is None:
|
||||||
|
return None
|
||||||
|
return await self.processQuery(interaction, searchResult)
|
||||||
|
|
||||||
|
async def search(self, query) -> str | None:
|
||||||
result = await self.youtube.searchVideo(query)
|
result = await self.youtube.searchVideo(query)
|
||||||
return result["result"][0]["link"]
|
if len(result) == 0:
|
||||||
|
return None
|
||||||
|
return result[0]["link"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user