Spaces:
Paused
Paused
Add all project files for Telegram Mini Web App
Browse files- Dockerfile +12 -0
- app.py +40 -0
- requirements.txt +6 -0
- static/index.html +76 -0
- telegram_bot.py +23 -0
- youtube_api.py +87 -0
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt ./
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 7860
|
| 11 |
+
|
| 12 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Query, HTTPException
|
| 2 |
+
from fastapi.responses import JSONResponse, RedirectResponse
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from youtube_api import YouTubeAPI
|
| 5 |
+
import uvicorn
|
| 6 |
+
|
| 7 |
+
app = FastAPI()
|
| 8 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 9 |
+
yt_api = YouTubeAPI()
|
| 10 |
+
|
| 11 |
+
@app.get("/")
|
| 12 |
+
def root():
|
| 13 |
+
return RedirectResponse(url="/static/index.html")
|
| 14 |
+
|
| 15 |
+
@app.get("/search")
|
| 16 |
+
async def search(q: str = Query(..., description="Search query")):
|
| 17 |
+
results = await yt_api.search(q, limit=10)
|
| 18 |
+
# Only return relevant fields
|
| 19 |
+
filtered = [
|
| 20 |
+
{
|
| 21 |
+
"title": r["title"],
|
| 22 |
+
"duration": r["duration"],
|
| 23 |
+
"id": r["id"],
|
| 24 |
+
"link": r["link"],
|
| 25 |
+
"thumbnails": r["thumbnails"],
|
| 26 |
+
}
|
| 27 |
+
for r in results
|
| 28 |
+
]
|
| 29 |
+
return JSONResponse(filtered)
|
| 30 |
+
|
| 31 |
+
@app.get("/stream")
|
| 32 |
+
async def stream(url: str = Query(..., description="YouTube video URL")):
|
| 33 |
+
try:
|
| 34 |
+
stream_url = await yt_api.get_stream_url(url, audio_only=True)
|
| 35 |
+
return RedirectResponse(url=stream_url)
|
| 36 |
+
except Exception as e:
|
| 37 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 38 |
+
|
| 39 |
+
if __name__ == "__main__":
|
| 40 |
+
uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
yt-dlp
|
| 4 |
+
youtubesearchpython
|
| 5 |
+
httpx
|
| 6 |
+
python-multipart
|
static/index.html
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Mini Web App Music Player</title>
|
| 7 |
+
<style>
|
| 8 |
+
body { font-family: Arial, sans-serif; margin: 2em; background: #f9f9f9; }
|
| 9 |
+
#results img { width: 80px; border-radius: 8px; }
|
| 10 |
+
#results { margin-top: 1em; }
|
| 11 |
+
.result { display: flex; align-items: center; margin-bottom: 1em; background: #fff; padding: 1em; border-radius: 8px; box-shadow: 0 2px 8px #0001; }
|
| 12 |
+
.info { margin-left: 1em; }
|
| 13 |
+
.play-btn { margin-left: auto; padding: 0.5em 1em; background: #007bff; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
|
| 14 |
+
.play-btn:hover { background: #0056b3; }
|
| 15 |
+
#player { margin-top: 2em; width: 100%; }
|
| 16 |
+
</style>
|
| 17 |
+
</head>
|
| 18 |
+
<body>
|
| 19 |
+
<h1>Mini Web App Music Player</h1>
|
| 20 |
+
<form id="searchForm">
|
| 21 |
+
<input type="text" id="query" placeholder="Search for a song or artist..." size="40" required>
|
| 22 |
+
<button type="submit">Search</button>
|
| 23 |
+
</form>
|
| 24 |
+
<div id="results"></div>
|
| 25 |
+
<audio id="player" controls style="display:none;"></audio>
|
| 26 |
+
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 27 |
+
<script>
|
| 28 |
+
// Telegram Web App integration
|
| 29 |
+
if (window.Telegram && Telegram.WebApp) {
|
| 30 |
+
Telegram.WebApp.ready();
|
| 31 |
+
document.body.style.background = Telegram.WebApp.themeParams.bg_color || '#f9f9f9';
|
| 32 |
+
// Optionally show user info
|
| 33 |
+
const user = Telegram.WebApp.initDataUnsafe.user;
|
| 34 |
+
if (user) {
|
| 35 |
+
const info = document.createElement('div');
|
| 36 |
+
info.innerHTML = `<b>Hi, ${user.first_name}${user.last_name ? ' ' + user.last_name : ''}!</b> <small>(via Telegram Mini App)</small>`;
|
| 37 |
+
document.body.insertBefore(info, document.body.firstChild.nextSibling);
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
const form = document.getElementById('searchForm');
|
| 41 |
+
const resultsDiv = document.getElementById('results');
|
| 42 |
+
const player = document.getElementById('player');
|
| 43 |
+
|
| 44 |
+
form.onsubmit = async (e) => {
|
| 45 |
+
e.preventDefault();
|
| 46 |
+
resultsDiv.innerHTML = 'Searching...';
|
| 47 |
+
const q = document.getElementById('query').value;
|
| 48 |
+
const res = await fetch(`/search?q=${encodeURIComponent(q)}`);
|
| 49 |
+
const results = await res.json();
|
| 50 |
+
if (!results.length) {
|
| 51 |
+
resultsDiv.innerHTML = 'No results found.';
|
| 52 |
+
return;
|
| 53 |
+
}
|
| 54 |
+
resultsDiv.innerHTML = '';
|
| 55 |
+
results.forEach(r => {
|
| 56 |
+
const div = document.createElement('div');
|
| 57 |
+
div.className = 'result';
|
| 58 |
+
div.innerHTML = `
|
| 59 |
+
<img src="${r.thumbnails[0].url}" alt="thumb">
|
| 60 |
+
<div class="info">
|
| 61 |
+
<div><b>${r.title}</b></div>
|
| 62 |
+
<div>Duration: ${r.duration || 'N/A'}</div>
|
| 63 |
+
</div>
|
| 64 |
+
<button class="play-btn">Play</button>
|
| 65 |
+
`;
|
| 66 |
+
div.querySelector('.play-btn').onclick = () => {
|
| 67 |
+
player.src = `/stream?url=${encodeURIComponent(r.link)}`;
|
| 68 |
+
player.style.display = 'block';
|
| 69 |
+
player.play();
|
| 70 |
+
};
|
| 71 |
+
resultsDiv.appendChild(div);
|
| 72 |
+
});
|
| 73 |
+
};
|
| 74 |
+
</script>
|
| 75 |
+
</body>
|
| 76 |
+
</html>
|
telegram_bot.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from telegram import Update, KeyboardButton, ReplyKeyboardMarkup
|
| 2 |
+
from telegram.ext import Application, CommandHandler, ContextTypes
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
# Set your bot token and web app URL here
|
| 6 |
+
BOT_TOKEN = os.getenv("BOT_TOKEN", "YOUR_BOT_TOKEN")
|
| 7 |
+
WEB_APP_URL = os.getenv("WEB_APP_URL", "https://xfasfadgagsg--miniwebapp.hf.space") # Replace with your actual Hugging Face Space URL
|
| 8 |
+
|
| 9 |
+
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
| 10 |
+
keyboard = [
|
| 11 |
+
[KeyboardButton("🎵 Open Music Player", web_app={"url": WEB_APP_URL})]
|
| 12 |
+
]
|
| 13 |
+
reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True)
|
| 14 |
+
await update.message.reply_text(
|
| 15 |
+
"Welcome! Tap below to open the music player:",
|
| 16 |
+
reply_markup=reply_markup
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
if __name__ == "__main__":
|
| 20 |
+
app = Application.builder().token(BOT_TOKEN).build()
|
| 21 |
+
app.add_handler(CommandHandler("start", start))
|
| 22 |
+
print("Bot is running. Send /start to your bot in Telegram.")
|
| 23 |
+
app.run_polling()
|
youtube_api.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
+
import re
|
| 4 |
+
import httpx
|
| 5 |
+
import yt_dlp
|
| 6 |
+
from youtubesearchpython.__future__ import VideosSearch
|
| 7 |
+
import tempfile
|
| 8 |
+
from typing import Union
|
| 9 |
+
|
| 10 |
+
async def shell_cmd(cmd):
|
| 11 |
+
proc = await asyncio.create_subprocess_shell(
|
| 12 |
+
cmd,
|
| 13 |
+
stdout=asyncio.subprocess.PIPE,
|
| 14 |
+
stderr=asyncio.subprocess.PIPE,
|
| 15 |
+
)
|
| 16 |
+
out, errorz = await proc.communicate()
|
| 17 |
+
if errorz:
|
| 18 |
+
if "unavailable videos are hidden" in (errorz.decode("utf-8")).lower():
|
| 19 |
+
return out.decode("utf-8")
|
| 20 |
+
else:
|
| 21 |
+
return errorz.decode("utf-8")
|
| 22 |
+
return out.decode("utf-8")
|
| 23 |
+
|
| 24 |
+
class YouTubeAPI:
|
| 25 |
+
def __init__(self):
|
| 26 |
+
self.base = "https://www.youtube.com/watch?v="
|
| 27 |
+
self.regex = r"(?:youtube\.com|youtu\.be)"
|
| 28 |
+
self.status = "https://www.youtube.com/oembed?url="
|
| 29 |
+
self.listbase = "https://youtube.com/playlist?list="
|
| 30 |
+
self.reg = re.compile(r"\x1B(?:[@-Z\\-_]|\\[[0-?]*[ -/]*[@-~])")
|
| 31 |
+
|
| 32 |
+
async def exists(self, link: str, videoid: Union[bool, str] = None):
|
| 33 |
+
if videoid:
|
| 34 |
+
link = self.base + link
|
| 35 |
+
if re.search(self.regex, link):
|
| 36 |
+
return True
|
| 37 |
+
else:
|
| 38 |
+
return False
|
| 39 |
+
|
| 40 |
+
async def details(self, link: str, videoid: Union[bool, str] = None):
|
| 41 |
+
if videoid:
|
| 42 |
+
link = self.base + link
|
| 43 |
+
if "&" in link:
|
| 44 |
+
link = link.split("&")[0]
|
| 45 |
+
results = VideosSearch(link, limit=1)
|
| 46 |
+
for result in (await results.next())["result"]:
|
| 47 |
+
title = result["title"]
|
| 48 |
+
duration_min = result["duration"]
|
| 49 |
+
thumbnail = result["thumbnails"][0]["url"].split("?")[0]
|
| 50 |
+
vidid = result["id"]
|
| 51 |
+
if str(duration_min) == "None":
|
| 52 |
+
duration_sec = 0
|
| 53 |
+
else:
|
| 54 |
+
duration_sec = self.time_to_seconds(duration_min)
|
| 55 |
+
return title, duration_min, duration_sec, thumbnail, vidid
|
| 56 |
+
|
| 57 |
+
async def search(self, query: str, limit: int = 10):
|
| 58 |
+
results = VideosSearch(query, limit=limit)
|
| 59 |
+
return (await results.next())["result"]
|
| 60 |
+
|
| 61 |
+
async def get_stream_url(self, link: str, audio_only: bool = True):
|
| 62 |
+
if "&" in link:
|
| 63 |
+
link = link.split("&")[0]
|
| 64 |
+
ydl_opts = {
|
| 65 |
+
"quiet": True,
|
| 66 |
+
"format": "bestaudio/best" if audio_only else "best[height<=?720][width<=?1280]",
|
| 67 |
+
"noplaylist": True,
|
| 68 |
+
}
|
| 69 |
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
| 70 |
+
info = ydl.extract_info(link, download=False)
|
| 71 |
+
if audio_only:
|
| 72 |
+
for f in info["formats"]:
|
| 73 |
+
if f.get("acodec") != "none" and f.get("vcodec") == "none":
|
| 74 |
+
return f["url"]
|
| 75 |
+
return info["url"]
|
| 76 |
+
else:
|
| 77 |
+
return info["url"]
|
| 78 |
+
|
| 79 |
+
def time_to_seconds(self, time_str):
|
| 80 |
+
parts = time_str.split(":")
|
| 81 |
+
parts = [int(p) for p in parts]
|
| 82 |
+
if len(parts) == 3:
|
| 83 |
+
return parts[0] * 3600 + parts[1] * 60 + parts[2]
|
| 84 |
+
elif len(parts) == 2:
|
| 85 |
+
return parts[0] * 60 + parts[1]
|
| 86 |
+
else:
|
| 87 |
+
return int(parts[0])
|