AbhiCalls 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- AbhiCalls/__init__.py +5 -0
- AbhiCalls/controller.py +96 -0
- AbhiCalls/models.py +29 -0
- AbhiCalls/player.py +83 -0
- AbhiCalls/queue.py +53 -0
- AbhiCalls/runtime.py +37 -0
- AbhiCalls/vc.py +11 -0
- AbhiCalls/yt.py +84 -0
- abhicalls-1.0.0.dist-info/METADATA +40 -0
- abhicalls-1.0.0.dist-info/RECORD +12 -0
- abhicalls-1.0.0.dist-info/WHEEL +5 -0
- abhicalls-1.0.0.dist-info/top_level.txt +1 -0
AbhiCalls/__init__.py
ADDED
AbhiCalls/controller.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from AbhiCalls.yt import resolve_query
|
|
2
|
+
from AbhiCalls.models import Song
|
|
3
|
+
from AbhiCalls.player import Player
|
|
4
|
+
|
|
5
|
+
class VoiceController:
|
|
6
|
+
def __init__(self, engine):
|
|
7
|
+
self.engine = engine
|
|
8
|
+
self.player = Player(engine)
|
|
9
|
+
self.engine.on_end = self._on_end
|
|
10
|
+
self.plugins = []
|
|
11
|
+
|
|
12
|
+
def load_plugin(self, plugin):
|
|
13
|
+
self.plugins.append(plugin)
|
|
14
|
+
|
|
15
|
+
async def _hook(self, name, *args):
|
|
16
|
+
for p in self.plugins:
|
|
17
|
+
fn = getattr(p, name, None)
|
|
18
|
+
if fn:
|
|
19
|
+
try:
|
|
20
|
+
await fn(*args)
|
|
21
|
+
except Exception as e:
|
|
22
|
+
print(f"[Plugin:{p.name}] {name} error:", e)
|
|
23
|
+
|
|
24
|
+
async def play(self, chat_id, query, requested_by):
|
|
25
|
+
results = await resolve_query(query)
|
|
26
|
+
if not results:
|
|
27
|
+
return None, None
|
|
28
|
+
|
|
29
|
+
first_pos = None
|
|
30
|
+
last_song = None
|
|
31
|
+
|
|
32
|
+
for data in results:
|
|
33
|
+
song = Song(
|
|
34
|
+
title=data["title"],
|
|
35
|
+
url=data["url"],
|
|
36
|
+
duration=data["duration"],
|
|
37
|
+
views=data["views"],
|
|
38
|
+
stream=data["stream"],
|
|
39
|
+
requested_by=requested_by,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
pos = await self.player.play(chat_id, song)
|
|
43
|
+
await self._hook("on_queue_add", chat_id, song, pos)
|
|
44
|
+
|
|
45
|
+
if first_pos is None:
|
|
46
|
+
first_pos = pos
|
|
47
|
+
last_song = song
|
|
48
|
+
|
|
49
|
+
if first_pos == 1:
|
|
50
|
+
await self._hook("on_song_start", chat_id, last_song)
|
|
51
|
+
|
|
52
|
+
return last_song, first_pos
|
|
53
|
+
|
|
54
|
+
async def skip(self, chat_id):
|
|
55
|
+
await self.player.skip(chat_id)
|
|
56
|
+
|
|
57
|
+
async def previous(self, chat_id):
|
|
58
|
+
return await self.player.previous(chat_id)
|
|
59
|
+
|
|
60
|
+
async def pause(self, chat_id):
|
|
61
|
+
await self.player.pause(chat_id)
|
|
62
|
+
|
|
63
|
+
async def resume(self, chat_id):
|
|
64
|
+
await self.player.resume(chat_id)
|
|
65
|
+
|
|
66
|
+
async def stop(self, chat_id):
|
|
67
|
+
await self.player.stop(chat_id)
|
|
68
|
+
|
|
69
|
+
async def mute(self, chat_id):
|
|
70
|
+
await self.player.mute(chat_id)
|
|
71
|
+
|
|
72
|
+
async def unmute(self, chat_id):
|
|
73
|
+
await self.player.unmute(chat_id)
|
|
74
|
+
|
|
75
|
+
async def volume(self, chat_id, volume: int):
|
|
76
|
+
await self.player.volume(chat_id, volume)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def loop(self, chat_id, count=None):
|
|
80
|
+
return self.player.set_loop(chat_id, count)
|
|
81
|
+
|
|
82
|
+
def eta(self, chat_id):
|
|
83
|
+
return self.player.eta(chat_id)
|
|
84
|
+
|
|
85
|
+
async def _on_end(self, chat_id):
|
|
86
|
+
q = self.player.queues.get(chat_id)
|
|
87
|
+
song = q.current() if q else None
|
|
88
|
+
if song:
|
|
89
|
+
await self._hook("on_song_end", chat_id, song)
|
|
90
|
+
|
|
91
|
+
await self.player.skip(chat_id)
|
|
92
|
+
|
|
93
|
+
next_song = q.current() if q else None
|
|
94
|
+
if next_song:
|
|
95
|
+
await self._hook("on_song_start", chat_id, next_song)
|
|
96
|
+
|
AbhiCalls/models.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class Song:
|
|
2
|
+
def __init__(
|
|
3
|
+
self,
|
|
4
|
+
title,
|
|
5
|
+
url,
|
|
6
|
+
duration,
|
|
7
|
+
views,
|
|
8
|
+
stream,
|
|
9
|
+
requested_by,
|
|
10
|
+
loop_left=0,
|
|
11
|
+
):
|
|
12
|
+
self.title = title
|
|
13
|
+
self.url = url
|
|
14
|
+
self.duration = duration
|
|
15
|
+
self.views = views
|
|
16
|
+
self.stream = stream
|
|
17
|
+
self.requested_by = requested_by
|
|
18
|
+
self.loop_left = loop_left
|
|
19
|
+
self.duration_sec = self._to_seconds(duration)
|
|
20
|
+
|
|
21
|
+
def _to_seconds(self, d):
|
|
22
|
+
try:
|
|
23
|
+
if ":" in d:
|
|
24
|
+
m, s = map(int, d.split(":"))
|
|
25
|
+
return m * 60 + s
|
|
26
|
+
return int(d)
|
|
27
|
+
except Exception:
|
|
28
|
+
return 0
|
|
29
|
+
|
AbhiCalls/player.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from AbhiCalls.queue import SongQueue
|
|
3
|
+
|
|
4
|
+
class Player:
|
|
5
|
+
def __init__(self, engine):
|
|
6
|
+
self.engine = engine
|
|
7
|
+
self.queues = {}
|
|
8
|
+
self.start_time = {}
|
|
9
|
+
|
|
10
|
+
def _queue(self, chat_id):
|
|
11
|
+
return self.queues.setdefault(chat_id, SongQueue())
|
|
12
|
+
|
|
13
|
+
async def play(self, chat_id, song):
|
|
14
|
+
q = self._queue(chat_id)
|
|
15
|
+
pos = q.add(song)
|
|
16
|
+
|
|
17
|
+
if pos == 1:
|
|
18
|
+
self.start_time[chat_id] = time.time()
|
|
19
|
+
await self.engine.play(chat_id, song.stream)
|
|
20
|
+
|
|
21
|
+
return pos
|
|
22
|
+
|
|
23
|
+
async def skip(self, chat_id):
|
|
24
|
+
q = self.queues.get(chat_id)
|
|
25
|
+
if not q:
|
|
26
|
+
return
|
|
27
|
+
nxt = q.next()
|
|
28
|
+
if nxt:
|
|
29
|
+
self.start_time[chat_id] = time.time()
|
|
30
|
+
await self.engine.play(chat_id, nxt.stream)
|
|
31
|
+
else:
|
|
32
|
+
await self.engine.stop(chat_id)
|
|
33
|
+
|
|
34
|
+
async def previous(self, chat_id):
|
|
35
|
+
q = self.queues.get(chat_id)
|
|
36
|
+
if not q:
|
|
37
|
+
return None
|
|
38
|
+
prev = q.previous()
|
|
39
|
+
if prev:
|
|
40
|
+
self.start_time[chat_id] = time.time()
|
|
41
|
+
await self.engine.play(chat_id, prev.stream)
|
|
42
|
+
return prev
|
|
43
|
+
|
|
44
|
+
async def stop(self, chat_id):
|
|
45
|
+
if chat_id in self.queues:
|
|
46
|
+
self.queues[chat_id].clear()
|
|
47
|
+
await self.engine.stop(chat_id)
|
|
48
|
+
|
|
49
|
+
async def pause(self, chat_id):
|
|
50
|
+
await self.engine.pause(chat_id)
|
|
51
|
+
|
|
52
|
+
async def resume(self, chat_id):
|
|
53
|
+
await self.engine.resume(chat_id)
|
|
54
|
+
|
|
55
|
+
async def mute(self, chat_id):
|
|
56
|
+
await self.engine.mute(chat_id)
|
|
57
|
+
|
|
58
|
+
async def unmute(self, chat_id):
|
|
59
|
+
await self.engine.unmute(chat_id)
|
|
60
|
+
|
|
61
|
+
async def volume(self, chat_id, volume: int):
|
|
62
|
+
await self.engine.change_volume(chat_id, volume)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def set_loop(self, chat_id, count=None):
|
|
66
|
+
q = self._queue(chat_id)
|
|
67
|
+
if count is None:
|
|
68
|
+
q.infinite_loop = not q.infinite_loop
|
|
69
|
+
return q.infinite_loop
|
|
70
|
+
|
|
71
|
+
cur = q.current()
|
|
72
|
+
if cur:
|
|
73
|
+
cur.loop_left = count - 1
|
|
74
|
+
return count
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
def eta(self, chat_id):
|
|
78
|
+
q = self.queues.get(chat_id)
|
|
79
|
+
if not q or not q.current():
|
|
80
|
+
return None
|
|
81
|
+
elapsed = int(time.time() - self.start_time.get(chat_id, 0))
|
|
82
|
+
return max(q.current().duration_sec - elapsed, 0)
|
|
83
|
+
|
AbhiCalls/queue.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
class SongQueue:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.items = []
|
|
7
|
+
self.history = deque(maxlen=20)
|
|
8
|
+
self.infinite_loop = False
|
|
9
|
+
|
|
10
|
+
def add(self, song):
|
|
11
|
+
self.items.append(song)
|
|
12
|
+
return len(self.items)
|
|
13
|
+
|
|
14
|
+
def current(self):
|
|
15
|
+
return self.items[0] if self.items else None
|
|
16
|
+
|
|
17
|
+
def next(self):
|
|
18
|
+
if not self.items:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
current = self.items[0]
|
|
22
|
+
self.history.appendleft(current)
|
|
23
|
+
|
|
24
|
+
if current.loop_left > 0:
|
|
25
|
+
current.loop_left -= 1
|
|
26
|
+
return current
|
|
27
|
+
|
|
28
|
+
if self.infinite_loop:
|
|
29
|
+
return current
|
|
30
|
+
|
|
31
|
+
self.items.pop(0)
|
|
32
|
+
return self.current()
|
|
33
|
+
|
|
34
|
+
def previous(self):
|
|
35
|
+
if not self.history:
|
|
36
|
+
return None
|
|
37
|
+
prev = self.history.popleft()
|
|
38
|
+
self.items.insert(0, prev)
|
|
39
|
+
return prev
|
|
40
|
+
|
|
41
|
+
def shuffle(self):
|
|
42
|
+
if len(self.items) <= 1:
|
|
43
|
+
return False
|
|
44
|
+
current = self.items.pop(0)
|
|
45
|
+
random.shuffle(self.items)
|
|
46
|
+
self.items.insert(0, current)
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
def clear(self):
|
|
50
|
+
self.items.clear()
|
|
51
|
+
self.history.clear()
|
|
52
|
+
self.infinite_loop = False
|
|
53
|
+
|
AbhiCalls/runtime.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from pytgcalls import idle as _idle
|
|
6
|
+
|
|
7
|
+
# ─────────────────────────────
|
|
8
|
+
# Mute pytgcalls logs
|
|
9
|
+
# ─────────────────────────────
|
|
10
|
+
logging.getLogger("pytgcalls").setLevel(logging.CRITICAL)
|
|
11
|
+
logging.getLogger("ntgcalls").setLevel(logging.CRITICAL)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@contextmanager
|
|
15
|
+
def _suppress_stdout():
|
|
16
|
+
"""
|
|
17
|
+
Temporarily suppress stdout/stderr
|
|
18
|
+
(used to hide pytgcalls banner)
|
|
19
|
+
"""
|
|
20
|
+
with open(os.devnull, "w") as devnull:
|
|
21
|
+
old_out = sys.stdout
|
|
22
|
+
old_err = sys.stderr
|
|
23
|
+
sys.stdout = devnull
|
|
24
|
+
sys.stderr = devnull
|
|
25
|
+
try:
|
|
26
|
+
yield
|
|
27
|
+
finally:
|
|
28
|
+
sys.stdout = old_out
|
|
29
|
+
sys.stderr = old_err
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def idle():
|
|
33
|
+
"""
|
|
34
|
+
Keep app running (pytgcalls idle hidden)
|
|
35
|
+
"""
|
|
36
|
+
with _suppress_stdout():
|
|
37
|
+
await _idle()
|
AbhiCalls/vc.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from AbhiCalls._engine.native import _NativeEngine
|
|
2
|
+
from AbhiCalls.controller import VoiceController
|
|
3
|
+
|
|
4
|
+
class VoiceEngine:
|
|
5
|
+
def __init__(self, app):
|
|
6
|
+
self._engine = _NativeEngine(app)
|
|
7
|
+
self.vc = VoiceController(self._engine)
|
|
8
|
+
|
|
9
|
+
async def start(self):
|
|
10
|
+
await self._engine.start()
|
|
11
|
+
|
AbhiCalls/yt.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from YouTubeMusic.Search import Search
|
|
3
|
+
from YouTubeMusic.Stream import get_stream
|
|
4
|
+
from YouTubeMusic.Playlist import get_playlist_songs
|
|
5
|
+
|
|
6
|
+
PLAYLIST_REGEX = re.compile(r"(list=)")
|
|
7
|
+
YOUTUBE_REGEX = re.compile(r"(youtube\.com|youtu\.be|music\.youtube\.com)")
|
|
8
|
+
|
|
9
|
+
def sec_to_mmss(sec):
|
|
10
|
+
try:
|
|
11
|
+
sec = int(sec)
|
|
12
|
+
return f"{sec // 60}:{sec % 60:02d}"
|
|
13
|
+
except Exception:
|
|
14
|
+
return "Unknown"
|
|
15
|
+
|
|
16
|
+
def yt_thumbnail(url: str):
|
|
17
|
+
"""Extract thumbnail from YouTube URL"""
|
|
18
|
+
try:
|
|
19
|
+
if "watch?v=" in url:
|
|
20
|
+
vid = url.split("watch?v=")[1].split("&")[0]
|
|
21
|
+
elif "youtu.be/" in url:
|
|
22
|
+
vid = url.split("youtu.be/")[1].split("?")[0]
|
|
23
|
+
else:
|
|
24
|
+
return None
|
|
25
|
+
return f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg"
|
|
26
|
+
except Exception:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def resolve_query(query: str):
|
|
31
|
+
songs = []
|
|
32
|
+
|
|
33
|
+
# 🎵 PLAYLIST
|
|
34
|
+
if PLAYLIST_REGEX.search(query):
|
|
35
|
+
playlist = await get_playlist_songs(query)
|
|
36
|
+
for item in playlist:
|
|
37
|
+
stream = get_stream(item["url"])
|
|
38
|
+
if not stream:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
songs.append({
|
|
42
|
+
"title": item.get("title", "Unknown"),
|
|
43
|
+
"url": item["url"],
|
|
44
|
+
"duration": sec_to_mmss(item.get("duration", "0")),
|
|
45
|
+
"views": "Unknown",
|
|
46
|
+
"thumbnail": yt_thumbnail(item["url"]),
|
|
47
|
+
"stream": stream,
|
|
48
|
+
})
|
|
49
|
+
return songs or None
|
|
50
|
+
|
|
51
|
+
# 🔗 DIRECT YT LINK
|
|
52
|
+
if YOUTUBE_REGEX.search(query):
|
|
53
|
+
stream = get_stream(query)
|
|
54
|
+
if not stream:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
return [{
|
|
58
|
+
"title": "YouTube Audio",
|
|
59
|
+
"url": query,
|
|
60
|
+
"duration": "Unknown",
|
|
61
|
+
"views": "Unknown",
|
|
62
|
+
"thumbnail": yt_thumbnail(query),
|
|
63
|
+
"stream": stream,
|
|
64
|
+
}]
|
|
65
|
+
|
|
66
|
+
# 🔍 SEARCH
|
|
67
|
+
res = await Search(query, limit=1)
|
|
68
|
+
if not res or not res.get("main_results"):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
i = res["main_results"][0]
|
|
72
|
+
stream = get_stream(i["url"])
|
|
73
|
+
if not stream:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
return [{
|
|
77
|
+
"title": i["title"],
|
|
78
|
+
"url": i["url"],
|
|
79
|
+
"duration": i.get("duration", "Unknown"),
|
|
80
|
+
"views": i.get("views", "Unknown"),
|
|
81
|
+
# 🔥 thumbnail from search (fallback safe)
|
|
82
|
+
"thumbnail": i.get("thumbnail") or yt_thumbnail(i["url"]),
|
|
83
|
+
"stream": stream,
|
|
84
|
+
}]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: AbhiCalls
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Telegram Voice Chat Music Engine with Queue, Loop, ETA and Player controls
|
|
5
|
+
Home-page: https://github.com/YouTubeMusicAPI/TgCall
|
|
6
|
+
Author: Abhishek Thakur
|
|
7
|
+
Author-email: Abhishek Thakur <abhishekbanshiwal2005@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/YouTubeMusicAPI/TgCall
|
|
10
|
+
Project-URL: Source, https://github.com/YouTubeMusicAPI/TgCall
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: py-tgcalls>=2.1.1
|
|
20
|
+
Requires-Dist: pyrogram>=2.0.0
|
|
21
|
+
Requires-Dist: telethon>=1.34.0
|
|
22
|
+
Requires-Dist: aiohttp
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
|
|
27
|
+
# AbhiCalls
|
|
28
|
+
|
|
29
|
+
AbhiCalls is a Telegram Voice Chat Music Engine built on top of PyTgCalls.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
- Play audio in Telegram voice chats
|
|
33
|
+
- Queue management
|
|
34
|
+
- Skip, pause, resume
|
|
35
|
+
- Loop and ETA support
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
```bash
|
|
39
|
+
pip install abhicalls
|
|
40
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
AbhiCalls/__init__.py,sha256=8Bgye84bSUJVFLhm2HPc2piO3YHZb67EihvJlGZxvDs,134
|
|
2
|
+
AbhiCalls/controller.py,sha256=mdowoTCc6jpDsdljuUWdgbsCMR5NObaw3Jh6LYb-O8I,2739
|
|
3
|
+
AbhiCalls/models.py,sha256=ybRrFbVmlpLyfmmn7lG2DpDz_Z9qcq3g9AD5Krc-UP0,664
|
|
4
|
+
AbhiCalls/player.py,sha256=UlAWmhZqHFjtAplA9-6lXFkVAu4pyEWDGbF8Wj9KYkM,2286
|
|
5
|
+
AbhiCalls/queue.py,sha256=iRrTvt1rU-diarSBCfxjeY-4uJ97oUsvDqtPpbB4UQY,1212
|
|
6
|
+
AbhiCalls/runtime.py,sha256=4_WmHirGwNOyn1Iuzbhecb_ao7qMDh1erFzp0ZaLze0,966
|
|
7
|
+
AbhiCalls/vc.py,sha256=ld3C4-QNDrHBQPrNzIuNTcu7zk1pVOo9rvrQBUsTI8s,309
|
|
8
|
+
AbhiCalls/yt.py,sha256=QnO2LdtgVbEi8XqKEY9ZSp6a6x1AjBYVCFNB2sXFcPU,2386
|
|
9
|
+
abhicalls-1.0.0.dist-info/METADATA,sha256=ubmOdWXkfYhqljgu7YAJDqOc28ENQNYSB5ea-qKxbuU,1216
|
|
10
|
+
abhicalls-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
abhicalls-1.0.0.dist-info/top_level.txt,sha256=3_ZgJEaE0rS9jpmEMi6oIXEtMzIagrn70XtK7H6w2gA,10
|
|
12
|
+
abhicalls-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AbhiCalls
|