Metadata-Version: 2.4
Name: akuibots-musical
Version: 1.0.1
Summary: Plugin musik untuk bot: kirim judul atau link (YouTube/Spotify) dapatkan thumbnail, judul, durasi, pembuat + audio stream
Author: Akuibots
License: MIT
Project-URL: Homepage, https://pypi.org/project/akuibots-musical/
Project-URL: Versi npm (Node.js), https://www.npmjs.com/package/akuibots-musical
Keywords: youtube,spotify,music,bot,audio,stream,yt-dlp,ytsearch,discord,telegram,voice,ffmpeg
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: yt-dlp>=2024.4.9
Requires-Dist: requests>=2.28.0
Dynamic: license-file

# 🎵 akuibots-musical (Python)

![PyPI — versi](https://img.shields.io/pypi/v/akuibots-musical) ![Python](https://img.shields.io/pypi/pyversions/akuibots-musical) ![Lisensi](https://img.shields.io/pypi/l/akuibots-musical)

**Plugin musik untuk bot** — bukan bot-nya. Kirim **judul** atau **link**
(YouTube / Spotify), plugin balas **metadata lengkap**: thumbnail, judul,
durasi, pembuat — **plus audio stream** yang siap diputar di voice bot.

> 🟢 Versi Node.js: [`akuibots-musical` di npm](https://www.npmjs.com/package/akuibots-musical) — API-nya sengaja dibuat mirip, tinggal pindah bahasa.

```python
from akuibots_musical import Musical

music = Musical()

track = music.play("a!p alan walker faded")   # judul → dicari otomatis di YouTube
track = music.play("https://youtu.be/60ItHLz5WEA")                          # link YouTube
track = music.play("https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC") # link Spotify

print(track.title)          # Alan Walker - Faded
print(track.author)         # Alan Walker
print(track.duration_text)  # 3:33
print(track.thumbnail)      # https://i.ytimg.com/vi/.../hqdefault.jpg

handle = music.get_stream(track)
print(handle.url)           # URL audio → pipa ke ffmpeg (voice bot)
```

## ✨ Fitur

- 🔎 **Kirim judul** → dicari otomatis di YouTube (kayak `ytsearch`), ambil hasil teratas
- 🔗 **Kirim link** → YouTube (watch / youtu.be / shorts / live / music) & Spotify (track / album / playlist / artist) langsung dikenali
- 🖼️ Data lengkap: thumbnail, judul, durasi, pembuat (uploader), views, status live
- 🔊 **Audio stream** siap dipipa ke ffmpeg — discord.py, py-cord, Telegram, CLI, apa saja
- ⚡ Tanpa Lavalink, tanpa API key, tanpa browser headless
- 🧹 Pembersih perintah otomatis: `a!p faded`, `!play faded`, `music-bot!p faded` → `faded`
- 🌐 Spotify tanpa API key (token anonim embed + fallback oEmbed) → dicocokkan ke YouTube untuk audionya
- 🐍 Dependency minimal: `yt-dlp` + `requests` — Python ≥ 3.8
- 🌀 Async ready: `play_async()` dkk. untuk bot berbasis asyncio (discord.py, dll)

## 📦 Instalasi

```bash
pip install akuibots-musical
```

Untuk memutar audio di voice bot kamu juga butuh **ffmpeg** terpasang di server
(`apt install ffmpeg` / `brew install ffmpeg`).

## 🚀 Quick Start

**1 — Instal:**

```bash
pip install akuibots-musical
```

**2 — Play dari judul (alur `a!p`):**

```python
from akuibots_musical import Musical

music = Musical()
track = music.play("a!p alan walker faded")
# prefix 'a!p' otomatis dibuang → dicari 'alan walker faded' di YouTube
```

**3 — Play dari link (YouTube / Spotify):**

```python
track = music.play("https://www.youtube.com/watch?v=60ItHLz5WEA")
track = music.play("https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC")
# Spotify → metadata diambil tanpa API key, lalu dicocokkan ke YouTube
```

**4 — Ambil audio untuk voice:**

```python
handle = music.get_stream(track)      # atau music.get_stream("a!p faded")
print(handle.url)                     # URL audio langsung (m4a/opus)
```

## 📦 Isi `Track`

| Field | Tipe | Contoh | Keterangan |
|---|---|---|---|
| `id` | `str` | `60ItHLz5WEA` | video ID YouTube |
| `title` | `str` | `Alan Walker - Faded` | judul |
| `author` | `str` | `Alan Walker` | pembuat / uploader |
| `duration` | `int \| None` | `213` | durasi dalam detik (`None` kalau live) |
| `duration_text` | `str` | `"3:33"` / `"Live"` | durasi siap tampil |
| `duration_ms` | property | `213000` | durasi dalam milidetik |
| `thumbnail` | `str` | `https://i.ytimg.com/vi/...` | gambar thumbnail |
| `url` | `str` | `https://www.youtube.com/watch?v=...` | halaman YouTube |
| `is_live` | `bool` | `False` | sedang live? |
| `view_count` | `int \| None` | `3500000000` | jumlah views |
| `source` | `str` | `"youtube"` / `"spotify"` | asal track |
| `spotify_url` | `str \| None` | | link Spotify asal (kalau dari Spotify) |
| `spotify_title` | `str \| None` | | judul asli di Spotify |
| `spotify_author` | `str \| None` | | artis asli di Spotify |
| `query` | `str \| None` | `alan walker faded` | judul yang diketik user |
| `to_dict()` | → `dict` | | semua field sebagai dict |

## 🔊 Isi `StreamHandle`

| Field / Method | Keterangan |
|---|---|
| `handle.url` | URL audio langsung (m4a/opus; live = HLS m3u8) → pipa ke ffmpeg |
| `handle.ext` / `handle.abr` / `handle.format_id` | info format audio |
| `handle.is_live` | `True` kalau live stream |
| `handle.track` | objek `Track` asalnya |
| `handle.iter_chunks(size=65536)` | generator byte audio (untuk relay manual) |
| `handle.save(path)` | unduh audio ke file |
| `handle.popen()` | subprocess yt-dlp → byte audio mentah di `proc.stdout` (paling tahan bot-check) |
| `handle.pcm_popen()` | yt-dlp → ffmpeg → PCM siap `discord.PCMAudio`; return `(proc_ytdlp, proc_ffmpeg)` |
| `handle.read_all()` | semua byte sekaligus (hati-hati memori) |

URL audio berlaku ± 6 jam dan terikat IP — ambil ulang (`get_stream`) setiap
kali memutar, jangan simpan lama.

## 🔎 Cara kerja — alur `a!p`

```
User : a!p faded
  └─> bot memanggil music.play("a!p faded")
        └─> buang prefix 'a!p'   → 'faded'
        └─> bukan link           → cari di YouTube (ytsearch)
        └─> ambil hasil teratas  → Track {title, author, duration, thumbnail, url}
  └─> bot balas embed (thumbnail + judul + durasi + channel)
  └─> music.get_stream(track) → URL audio → ffmpeg → voice channel
```

Link dikenali otomatis: YouTube → ambil langsung; Spotify → ambil metadata
lalu cocokkan ke YouTube; link lain → error `UNSUPPORTED_SOURCE`.

## 📖 API Reference

### `Musical(strip_prefix=True, default_limit=5, ytdlp_options=None)`

| Option | Default | Keterangan |
|---|---|---|
| `strip_prefix` | `True` | buang prefix perintah (`a!p`, `!play`, `bot!p`, ...) otomatis |
| `default_limit` | `5` | jumlah hasil default `search()` |
| `ytdlp_options` | `None` | opsi yt-dlp tambahan, mis. `{"cookiefile": "cookies.txt"}` |

### Metode

| Metode | Input | Return |
|---|---|---|
| `search(query, limit=None)` | judul | `list[Track]` |
| `resolve(text)` | judul / link (boleh berprefix `a!p`) | `Track` |
| `play(text)` | sama dengan `resolve` | `Track` |
| `get_stream(x)` | `Track` / judul / link | `StreamHandle` |
| `play_stream(x)` | `Track` / judul / link | `(Track, StreamHandle)` |
| `search_async`, `resolve_async`, `play_async`, `get_stream_async`, `play_stream_async` | sama | coroutine (asyncio) |

### Fungsi standalone (instance default)

```python
from akuibots_musical import play, resolve, search, get_stream, play_stream
track = play("a!p faded")
```

### Helper

`parse_command`, `strip_command_prefix`, `is_url`, `detect_source`,
`format_duration`, `extract_youtube_id`, `resolve_spotify` — dan
`MusicalError` dengan `.code`: `INVALID_INPUT`, `NOT_FOUND`, `SEARCH_FAILED`,
`UNSUPPORTED_SOURCE`, `SPOTIFY_FAILED`, `STREAM_FAILED`.

## 🤖 Contoh: Bot Discord (discord.py)

```python
import discord
from discord.ext import commands
from akuibots_musical import Musical

music = Musical()
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="a!", intents=intents)

@bot.event
async def on_ready():
    print("Bot siap:", bot.user)

@bot.command()
async def p(ctx, *, query: str):
    """a!p <judul/link> → play musik di voice channel."""
    if not ctx.author.voice:
        return await ctx.send("Masuk voice channel dulu ya 🎧")
    await ctx.send(f"🔍 Mencari: `{query}`")

    track = await music.play_async(query)          # judul ATAU link
    handle = await music.get_stream_async(track)   # metadata + audio

    if ctx.voice_client is None:
        await ctx.author.voice.channel.connect()

    # ✅ Pola paling awet (2025+): yt-dlp yang mengunduh audionya
    # (headers & PO token diurus yt-dlp), lalu dipipa ke ffmpeg.
    # URL mentah handle.url langsung ke ffmpeg sering DITOLAK YouTube
    # (bot jalan tapi tanpa suara).
    ytdlp_proc, ff_proc = handle.pcm_popen()
    source = discord.PCMAudio(ff_proc.stdout)

    def selesai(e):
        for p in (ytdlp_proc, ff_proc):
            try:
                p.kill()
            except Exception:
                pass
        if e:
            print("FFmpeg error:", e)

    ctx.voice_client.stop()
    ctx.voice_client.play(source, after=selesai)

    embed = discord.Embed(title=track.title, url=track.url, color=0x2B6CB0)
    embed.set_thumbnail(url=track.thumbnail)
    embed.add_field(name="Durasi", value=track.duration_text, inline=True)
    embed.add_field(name="Channel", value=track.author, inline=True)
    embed.set_footer(text=f"Sumber: {track.source}")
    await ctx.send(embed=embed)

bot.run("TOKEN_BOT")
```

## ✈️ Contoh: Bot Telegram (python-telegram-bot)

```python
from telegram.ext import Application, CommandHandler, ContextTypes
from telegram import Update
from akuibots_musical import Musical

music = Musical()

async def p(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = " ".join(context.args or [])
    if not query:
        return await update.message.reply_text("Kirim judul atau link: /p faded")
    msg = await update.message.reply_text(f"🔍 Mencari: {query}")
    track, handle = await music.play_stream_async(query)
    await msg.edit_text(
        f"🎵 {track.title}\n"
        f"👤 {track.author}\n"
        f"⏱ {track.duration_text}\n"
        f"🔗 {track.url}"
    )
    # opsional — kirim file audionya juga:
    # handle.save("lagu.m4a") lalu reply_audio

app = Application.builder().token("TOKEN_BOT").build()
app.add_handler(CommandHandler("p", p))
app.run_polling()
```

## 🖥️ Contoh: CLI

```python
#!/usr/bin/env python3
import subprocess, sys
from akuibots_musical import music

track, handle = music.play_stream(" ".join(sys.argv[1:]))
print(f"▶ {track.title} — {track.author}  [{track.duration_text}]")
subprocess.run(["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", handle.url])
```

```bash
python player.py "a!p alan walker faded"
```

## ⚙️ Opsi yt-dlp & cookies

Semua opsi yt-dlp bisa diteruskan lewat `ytdlp_options`:

```python
music = Musical(ytdlp_options={
    "cookiefile": "cookies.txt",                       # cookies browser (Netscape)
    "extractor_args": {"youtube": {"player_client": ["ios", "web"]}},
})
```

## 🆘 Troubleshooting

| Masalah | Penyebab & solusi |
|---|---|
| `Sign in to confirm you're not a bot` | YouTube menandai IP server/VPS. Export cookies.txt dari browser (format Netscape) lalu `Musical(ytdlp_options={"cookiefile": "cookies.txt"})`. Di IP rumah biasanya langsung jalan. |
| `ffmpeg not found` | Pasang ffmpeg: `apt install ffmpeg` / `brew install ffmpeg`. |
| URL audio 403 setelah lama | URL kadaluarsa (±6 jam) & terikat IP. Panggil `get_stream()` ulang sebelum tiap memutar. |
| Bot jalan, embed muncul, tapi **tidak ada suara** | URL audio mentah ditolak YouTube saat diambil ffmpeg langsung (PO token/SABR — aturan baru YouTube 2025+). Solusi: ① `pip install -U yt-dlp` dan pasang [Deno](https://docs.deno.com/runtime/getting_started/installation/), ② pakai pola pipe `handle.pcm_popen()` pada contoh Discord (yt-dlp yang mengunduh audionya, bukan ffmpeg), ③ tes dengan script diagnosis di bawah. |
| `SPOTIFY_FAILED` | Link privat/regional atau diblokir. Coba link track langsung dari aplikasi Spotify (Share → Copy link). |
| `UNSUPPORTED_SOURCE` | Link bukan YouTube/Spotify. Plugin ini hanya menerima keduanya + judul. |
| Hasil pencarian kurang pas | Beri judul lebih spesifik, mis. `a!p alan walker faded` bukan `a!p faded`. |

## ❓ FAQ

**Ini bot Discord?**
Bukan. Ini **plugin/library** yang dipasang di bot kamu (Discord, Telegram,
WhatsApp, CLI, apa saja). Bot yang memproses perintah `a!p`, plugin ini yang
mengerjakan musiknya.

**Sama kayak Lavalink?**
Gak perlu server Lavalink, gak perlu Java, gak perlu API key. Cukup
`pip install akuibots-musical` — pencarian & stream langsung jalan.

**Kenapa Spotify cuma dapat metadata?**
Spotify tidak menyediakan audio untuk pihak ketiga. Plugin mengambil metadata
Spotify (judul + artis), lalu otomatis mencocokkannya ke YouTube — jadi
`music.play_stream(<link spotify>)` tetap menghasilkan audio.

**Bisa buat download mp3?**
Bisa — `handle.save("lagu.m4a")`, atau konversi pakai ffmpeg ke mp3.

**Bedanya dengan versi npm?**

| npm (Node.js) | PyPI (Python) |
|---|---|
| `new Musical({...})` | `Musical(strip_prefix=True, ...)` |
| `await music.play(text)` | `music.play(text)` / `await music.play_async(text)` |
| `await music.getStream(x)` | `music.get_stream(x)` |
| `track.durationText` | `track.duration_text` |
| Node `Readable` stream | `StreamHandle` (`url`, `iter_chunks()`, `save()`) |

**Pakai yt-dlp?** Ya — mesin pencarian & ekstraksi memakai `yt-dlp`
yang aktif dirawat, jadi jauh lebih tahan perubahan YouTube daripada
library ytdl-core lama.

## 📄 Lisensi

MIT — © 2026 Akuibots. Versi npm: [`akuibots-musical`](https://www.npmjs.com/package/akuibots-musical).
