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 ADDED
@@ -0,0 +1,5 @@
1
+ from AbhiCalls.vc import VoiceEngine
2
+ from AbhiCalls.runtime import idle
3
+ from .player import Player
4
+
5
+ __all__ = ["VoiceEngine", "idle"]
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ AbhiCalls