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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.7.1
1.8.0
4 changes: 3 additions & 1 deletion config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"Host": "127.0.0.1",
"Port": 27019,
"SampleRate": 22050,
"Volume": 1.0
"Volume": 1.0,
"Proxy": ""
},

"GeoIP":
Expand Down Expand Up @@ -222,6 +223,7 @@
}
],
"parameters": {
"proxy": "",
"keywords_banned": [
"earrape",
"rape",
Expand Down
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,9 @@ version = {file = "VERSION"}

[project.optional-dependencies]
dev = [
"ruff",
"memory_profiler",
"mypy",
"pip-tools",
"pyupgrade",
"ruff",
]

[tool.mypy]
Expand Down
20 changes: 1 addition & 19 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ beautifulsoup4==4.12.3
# via
# -c requirements.txt
# torchlight (pyproject.toml)
build==1.0.3
# via pip-tools
certifi==2023.11.17
# via
# -c requirements.txt
Expand All @@ -32,7 +30,6 @@ click==8.1.7
# -c requirements.txt
# torchlight (pyproject.toml)
# gtts
# pip-tools
defusedxml==0.7.1
# via
# -c requirements.txt
Expand Down Expand Up @@ -74,26 +71,16 @@ mypy==1.8.0
# via torchlight (pyproject.toml)
mypy-extensions==1.0.0
# via mypy
packaging==23.2
# via build
pillow==10.2.0
# via
# -c requirements.txt
# torchlight (pyproject.toml)
pip==24.3.1
# via pip-tools
pip-tools==7.3.0
# via torchlight (pyproject.toml)
psutil==6.1.1
# via memory-profiler
pyproject-hooks==1.0.0
# via build
python-magic==0.4.27
# via
# -c requirements.txt
# torchlight (pyproject.toml)
pyupgrade==3.15.0
# via torchlight (pyproject.toml)
requests==2.32.3
# via
# -c requirements.txt
Expand All @@ -106,26 +93,21 @@ setuptools==75.8.0
# -c requirements.txt
# geoip2
# maxminddb
# pip-tools
soupsieve==2.5
# via
# -c requirements.txt
# beautifulsoup4
tokenize-rt==5.2.0
# via pyupgrade
typing-extensions==4.9.0
# via mypy
urllib3==2.1.0
# via
# -c requirements.txt
# requests
wheel==0.42.0
# via pip-tools
yarl==1.9.4
# via
# -c requirements.txt
# aiohttp
yt-dlp @ git+https://github.com/yt-dlp/yt-dlp@af2c821d74049b519895288aca23cee81fc4b049#egg=yt-dlp
yt-dlp @ git+https://github.com/yt-dlp/yt-dlp@5ff7a43623e3a92270f66a7e37b5fc53d7a57fdf#egg=yt-dlp
# via
# -c requirements.txt
# torchlight (pyproject.toml)
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ urllib3==2.1.0
# via requests
yarl==1.9.4
# via aiohttp
yt-dlp @ git+https://github.com/yt-dlp/yt-dlp@af2c821d74049b519895288aca23cee81fc4b049#egg=yt-dlp
yt-dlp @ git+https://github.com/yt-dlp/yt-dlp@5ff7a43623e3a92270f66a7e37b5fc53d7a57fdf#egg=yt-dlp
# via torchlight (pyproject.toml)
4 changes: 2 additions & 2 deletions src/torchlight/AsyncClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(
# @profile
async def Connect(self) -> None:
while True:
self.logger.warn("Reconnecting...")
self.logger.warning("Reconnecting...")
try:
_, self.protocol = await self.loop.create_connection(
lambda: ClientProtocol(self.loop),
Expand Down Expand Up @@ -64,7 +64,7 @@ def OnReceive(self, data: str | bytes) -> None:
try:
json_obj = json.loads(data)
except Exception:
self.logger.warn("OnReceive: Unable to decode data as json, skipping")
self.logger.warning("OnReceive: Unable to decode data as json, skipping")
return

if "method" in json_obj and json_obj["method"] == "publish":
Expand Down
12 changes: 8 additions & 4 deletions src/torchlight/Commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,8 @@ async def _func(self, message: list[str], player: Player) -> int:
if self.check_disabled(player):
return -1

command_config = self.get_config()

input_keywords = message[1]
if URLFilter.youtube_regex.search(input_keywords):
input_url = input_keywords
Expand All @@ -760,8 +762,11 @@ async def _func(self, message: list[str], player: Player) -> int:

real_time = get_url_real_time(url=input_url)

if "parameters" in command_config and "proxy" in command_config["parameters"]:
proxy = command_config["parameters"]["proxy"]

try:
info = get_url_youtube_info(url=input_url)
info = get_url_youtube_info(url=input_url, proxy=proxy)
except Exception as e:
self.logger.error(f"Failed to extract youtube info from: {input_url}")
self.logger.error(e)
Expand All @@ -772,16 +777,15 @@ async def _func(self, message: list[str], player: Player) -> int:
return 1

if "title" not in info and "url" in info:
info = get_url_youtube_info(url=info["url"])
info = get_url_youtube_info(url=info["url"], proxy=proxy)
if info["extractor_key"] == "YoutubeSearch":
info = get_first_valid_entry(entries=info["entries"])
info = get_first_valid_entry(entries=info["entries"], proxy=proxy)

title = info["title"]
url = get_audio_format(info=info)
title_words = title.split()
keywords_banned: list[str] = []

command_config = self.get_config()
if "parameters" in command_config and "keywords_banned" in command_config["parameters"]:
keywords_banned = command_config["parameters"]["keywords_banned"]

Expand Down
152 changes: 96 additions & 56 deletions src/torchlight/FFmpegAudioPlayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ def __init__(self, torchlight: Torchlight) -> None:
self.port = self.config["Port"]
self.sample_rate = float(self.config["SampleRate"])
self.volume = float(self.config["Volume"])
self.proxy = self.config["Proxy"]

self.started_playing: float | None = None
self.stopped_playing: float | None = None
self.seconds = 0.0

self.writer: StreamWriter | None = None
self.sub_process: Process | None = None
self.ffmpeg_process: Process | None = None
self.curl_process: Process | None = None

self.callbacks: list[tuple[str, Callable]] = []

Expand All @@ -45,65 +47,69 @@ def __del__(self) -> None:

# @profile
def PlayURI(self, uri: str, position: int | None, *args: Any) -> bool:
curl_command = ["/usr/bin/curl", "-L", uri]
if self.proxy:
curl_command.extend(
[
"-x",
self.proxy,
]
)
ffmpeg_command = [
"/usr/bin/ffmpeg",
"-i",
"pipe:0",
"-acodec",
"pcm_s16le",
"-ac",
"1",
"-ar",
str(int(self.sample_rate)),
"-filter:a",
f"volume={str(float(self.volume))}",
"-f",
"s16le",
"-vn",
*args,
"-",
]

if position is not None:
pos_str = str(datetime.timedelta(seconds=position))
command = [
"/usr/bin/ffmpeg",
"-ss",
pos_str,
"-i",
uri,
"-acodec",
"pcm_s16le",
"-ac",
"1",
"-ar",
str(int(self.sample_rate)),
"-filter:a",
f"volume={str(float(self.volume))}",
"-f",
"s16le",
"-vn",
*args,
"-",
]
ffmpeg_command.extend(
[
"-ss",
pos_str,
]
)
self.position = position
else:
command = [
"/usr/bin/ffmpeg",
"-i",
uri,
"-acodec",
"pcm_s16le",
"-ac",
"1",
"-ar",
str(int(self.sample_rate)),
"-filter:a",
f"volume={str(float(self.volume))}",
"-f",
"s16le",
"-vn",
*args,
"-",
]

self.logger.debug(command)

self.logger.debug(curl_command)
self.logger.debug(ffmpeg_command)

self.playing = True
asyncio.ensure_future(self._stream_subprocess(command))
asyncio.ensure_future(self._stream_subprocess(curl_command, ffmpeg_command))
return True

# @profile
def Stop(self, force: bool = True) -> bool:
if not self.playing:
return False

if self.sub_process:
if self.ffmpeg_process:
try:
self.ffmpeg_process.terminate()
self.ffmpeg_process.kill()
self.ffmpeg_process = None
except ProcessLookupError as exc:
self.logger.debug(exc)
pass

if self.curl_process:
try:
self.sub_process.terminate()
self.sub_process.kill()
self.sub_process = None
self.curl_process.terminate()
self.curl_process.kill()
self.curl_process = None
except ProcessLookupError as exc:
self.logger.debug(exc)
pass
Expand All @@ -128,7 +134,7 @@ def Stop(self, force: bool = True) -> bool:
else:
loop.run_until_complete(self.writer.wait_closed())
except Exception as exc:
self.logger.warn(exc)
self.logger.warning(exc)
pass

self.playing = False
Expand Down Expand Up @@ -173,7 +179,7 @@ async def _updater(self) -> None:

if seconds_elapsed >= self.seconds:
if not self.stopped_playing:
self.logger.warn("BUFFER UNDERRUN!")
self.logger.debug("BUFFER UNDERRUN!")
self.Stop(False)
return

Expand Down Expand Up @@ -208,21 +214,55 @@ async def _read_stream(self, stream: StreamReader | None, writer: StreamWriter)
self.stopped_playing = time.time()

# @profile
async def _stream_subprocess(self, cmd: list[str]) -> None:
async def _stream_subprocess(self, curl_command: list[str], ffmpeg_command: list[str]) -> None:
if not self.playing:
return

_, self.writer = await asyncio.open_connection(self.host, self.port)

self.sub_process = await asyncio.create_subprocess_exec(
*cmd,
self.ffmpeg_process = await asyncio.create_subprocess_exec(
*ffmpeg_command,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)

self.curl_process = await asyncio.create_subprocess_exec(
*curl_command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)

await self._read_stream(self.sub_process.stdout, self.writer)
if self.sub_process is not None:
await self.sub_process.wait()
asyncio.create_task(self._wait_for_process_exit(self.curl_process))

asyncio.create_task(self._write_stream(self.curl_process.stdout, self.ffmpeg_process.stdin))

await self._read_stream(self.ffmpeg_process.stdout, self.writer)

if self.ffmpeg_process is not None:
if self.ffmpeg_process.stdin:
self.ffmpeg_process.stdin.close()
await self.ffmpeg_process.wait()

self.writer.close()
await self.writer.wait_closed()

if self.seconds == 0.0:
self.Stop()

async def _write_stream(self, stream: StreamReader | None, writer: StreamWriter | None) -> None:
while True:
if not stream:
break
chunk = await stream.read(65536)
if not chunk:
break

if writer:
writer.write(chunk)
await writer.drain()

async def _wait_for_process_exit(self, curl_process: Process) -> None:
await curl_process.wait()
if curl_process.returncode != 0:
self.logger.error(f"Curl process exited with error code {curl_process.returncode}")
self.Stop()
Loading
Loading