Compare commits
3 commits
feat/yt-mu
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5666b87124 | ||
![]() |
d265b92f1d | ||
![]() |
4bd6ad85e3 |
12 changed files with 312 additions and 462 deletions
|
@ -1,57 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import yt_dlp
|
|
||||||
|
|
||||||
from music_player import Track
|
|
||||||
|
|
||||||
ydl_opts = {
|
|
||||||
"format": "mp3/bestaudio/best",
|
|
||||||
# ℹ️ See help(yt_dlp.postprocessor) for a list of available Postprocessors and their arguments
|
|
||||||
"postprocessors": [
|
|
||||||
{
|
|
||||||
"key": "FFmpegExtractAudio",
|
|
||||||
"preferredcodec": "mp3",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {"home": "queue"},
|
|
||||||
"outtmpl": {"default": "%(artist)s - %(track)s [%(id)s].%(ext)s"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DownloadService:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.ydl = yt_dlp.YoutubeDL(ydl_opts)
|
|
||||||
|
|
||||||
async def download(self, url: str) -> Track:
|
|
||||||
def extract():
|
|
||||||
return self.ydl.extract_info(url, download=True)
|
|
||||||
|
|
||||||
info = await asyncio.to_thread(extract)
|
|
||||||
|
|
||||||
try:
|
|
||||||
filepath = info["requested_downloads"][-1]["filepath"] # type: ignore
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError("Could not ")
|
|
||||||
track = Track(
|
|
||||||
artist=info.get("artist", None), # type: ignore
|
|
||||||
title=info.get("title", None), # type: ignore
|
|
||||||
duration=info.get("duration", None), # type: ignore
|
|
||||||
filepath=filepath, # type: ignore
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Finished processing {track}")
|
|
||||||
|
|
||||||
return track
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
qser = DownloadService()
|
|
||||||
qser.download(sys.argv[1])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
148
index.html
148
index.html
|
@ -1,148 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Music Player</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; padding: 1rem; }
|
|
||||||
button { margin: 0.5rem; }
|
|
||||||
#queueList { margin-top: 1rem; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div style="font-family: sans-serif; max-width: 600px; margin: auto;">
|
|
||||||
<h2>🎵 Dashdio</h2>
|
|
||||||
<div id="nowPlaying">
|
|
||||||
<strong>Artist:</strong> <span id="trackArtist">-</span><br>
|
|
||||||
<strong>Title:</strong> <span id="trackTitle">-</span><br>
|
|
||||||
<strong>Status:</strong> <span id="playbackState">-</span><br>
|
|
||||||
<div style="margin-top: 10px;">
|
|
||||||
<progress id="progressBar" value="0" max="100" style="width: 100%; height: 20px;"></progress>
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<span id="elapsedTime">0:00</span> / <span id="totalTime">0:00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<label for="volumeSlider"><strong>🔊 Volume:</strong> <span id="volumeValue">1</span></label><br>
|
|
||||||
<input type="range" min="0" max="1" step="0.01" id="volumeSlider" oninput="setVolume(this.value)">
|
|
||||||
<button onclick="play()">Play/Pause</button>
|
|
||||||
<button onclick="stop()">Stop</button>
|
|
||||||
<button onclick="skip()">Skip</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 style="margin-top: 30px;">📃 Queue</h3>
|
|
||||||
<input type="text" id="trackUrl" placeholder="Track URL">
|
|
||||||
<button onclick="addToQueue()">Add to Queue</button>
|
|
||||||
<table id="queueTable" style="width: 100%; border-collapse: collapse; font-size: 0.95em;">
|
|
||||||
<thead>
|
|
||||||
<tr style="background-color: #f0f0f0;">
|
|
||||||
<th style="text-align: left; padding: 8px;">#</th>
|
|
||||||
<th style="text-align: left; padding: 8px;">Artist</th>
|
|
||||||
<th style="text-align: left; padding: 8px;">Title</th>
|
|
||||||
<th style="text-align: right; padding: 8px;">Duration</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="queueBody">
|
|
||||||
<!-- Filled by JS -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
function formatTime(seconds) {
|
|
||||||
const m = Math.floor(seconds / 60);
|
|
||||||
const s = Math.floor(seconds % 60).toString().padStart(2, '0');
|
|
||||||
return `${m}:${s}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgress(position, duration) {
|
|
||||||
const progressBar = document.getElementById("progressBar");
|
|
||||||
const elapsedTime = document.getElementById("elapsedTime");
|
|
||||||
const totalTime = document.getElementById("totalTime");
|
|
||||||
|
|
||||||
progressBar.max = duration;
|
|
||||||
progressBar.value = position;
|
|
||||||
elapsedTime.textContent = formatTime(position);
|
|
||||||
totalTime.textContent = formatTime(duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const api = (endpoint, options = {}) =>
|
|
||||||
fetch(endpoint, options).then(res => res.json()).catch(console.error);
|
|
||||||
|
|
||||||
const setVolume = async (val) => {
|
|
||||||
await api(`/player/volume?volume=${val}`, { method: "PUT" });
|
|
||||||
document.getElementById("volumeValue").textContent = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addToQueue = async () => {
|
|
||||||
const url = document.getElementById("trackUrl").value;
|
|
||||||
await api(`/queue?url=${encodeURIComponent(url)}`, { method: "POST" });
|
|
||||||
updateQueue();
|
|
||||||
};
|
|
||||||
|
|
||||||
const play = async () => { await api("/player/play", { method: "POST" });};
|
|
||||||
const stop = async () => { await api("/player/stop", { method: "POST" });};
|
|
||||||
const skip = async () => { await api("/player/skip", { method: "POST" });};
|
|
||||||
|
|
||||||
// WebSocket connections
|
|
||||||
let playerSocket, queueSocket;
|
|
||||||
|
|
||||||
function connectWebSockets() {
|
|
||||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
|
||||||
const base = `${proto}://${location.host}`;
|
|
||||||
|
|
||||||
playerSocket = new WebSocket(`${base}/player`);
|
|
||||||
queueSocket = new WebSocket(`${base}/queue`);
|
|
||||||
|
|
||||||
playerSocket.onopen = () => playerSocket.send("ping");
|
|
||||||
queueSocket.onopen = () => queueSocket.send("ping");
|
|
||||||
|
|
||||||
playerSocket.onmessage = (event) => {
|
|
||||||
const state = JSON.parse(event.data);
|
|
||||||
|
|
||||||
const { playback_state, track, position, volume } = state;
|
|
||||||
|
|
||||||
document.getElementById("trackArtist").textContent = track?.artist || "-";
|
|
||||||
document.getElementById("trackTitle").textContent = track?.title || "-";
|
|
||||||
document.getElementById("playbackState").textContent = playback_state;
|
|
||||||
|
|
||||||
document.getElementById("volumeSlider").value = volume;
|
|
||||||
document.getElementById("volumeValue").textContent = volume;
|
|
||||||
|
|
||||||
if (track) {
|
|
||||||
updateProgress(position, track.duration);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
queueSocket.onmessage = (event) => {
|
|
||||||
const queue = JSON.parse(event.data);
|
|
||||||
const queueBody = document.getElementById("queueBody");
|
|
||||||
queueBody.innerHTML = "";
|
|
||||||
|
|
||||||
(queue.items || queue).forEach((track, index) => {
|
|
||||||
const row = document.createElement("tr");
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<td style="padding: 8px;">${index + 1}</td>
|
|
||||||
<td style="padding: 8px;">${track.artist}</td>
|
|
||||||
<td style="padding: 8px;">${track.title}</td>
|
|
||||||
<td style="padding: 8px; text-align: right;">${formatTime(track.duration)}</td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
queueBody.appendChild(row);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
playerSocket.onerror = queueSocket.onerror = console.error;
|
|
||||||
playerSocket.onclose = () => setTimeout(connectWebSockets, 1000);
|
|
||||||
queueSocket.onclose = () => setTimeout(connectWebSockets, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
connectWebSockets();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
92
main.py
92
main.py
|
@ -1,62 +1,10 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from download_service import DownloadService
|
|
||||||
from music_player import MusicPlayer, PlayerState
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePlayerState(Enum):
|
|
||||||
play = "play"
|
|
||||||
pause = "pause"
|
|
||||||
resume = "resume"
|
|
||||||
stop = "stop"
|
|
||||||
|
|
||||||
|
|
||||||
class WSConnectionType(Enum):
|
|
||||||
state = "state"
|
|
||||||
queue = "queue"
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionManager:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.active_connections: dict[str, set[WebSocket]] = {
|
|
||||||
WSConnectionType.state.value: set(),
|
|
||||||
WSConnectionType.queue.value: set(),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def connect(self, websocket: WebSocket, type: WSConnectionType):
|
|
||||||
await websocket.accept()
|
|
||||||
self.active_connections[type.value].add(websocket)
|
|
||||||
|
|
||||||
async def send(self, ws: WebSocket, message: BaseModel):
|
|
||||||
try:
|
|
||||||
await ws.send_json(message.model_dump())
|
|
||||||
except Exception:
|
|
||||||
self.disconnect(ws)
|
|
||||||
|
|
||||||
async def broadcast(self, ws_type: WSConnectionType, message: BaseModel):
|
|
||||||
broken = set()
|
|
||||||
conn_list = list(self.active_connections[ws_type.value])
|
|
||||||
for idx in range(len(conn_list)):
|
|
||||||
ws = conn_list[idx]
|
|
||||||
try:
|
|
||||||
await ws.send_json(message.model_dump())
|
|
||||||
except Exception:
|
|
||||||
broken.add(ws)
|
|
||||||
for ws in broken:
|
|
||||||
self.disconnect(ws)
|
|
||||||
|
|
||||||
def disconnect(self, websocket: WebSocket):
|
|
||||||
if websocket in self.active_connections[WSConnectionType.state.value]:
|
|
||||||
self.active_connections[WSConnectionType.state.value].remove(websocket)
|
|
||||||
|
|
||||||
if websocket in self.active_connections[WSConnectionType.queue.value]:
|
|
||||||
self.active_connections[WSConnectionType.queue.value].remove(websocket)
|
|
||||||
|
|
||||||
|
from models import MusicPlayer, PlayerState, WSConnectionType
|
||||||
|
from services import ConnectionManager, DownloadService
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
tags_metadata = [
|
tags_metadata = [
|
||||||
|
@ -64,17 +12,16 @@ tags_metadata = [
|
||||||
{"name": "experimental"},
|
{"name": "experimental"},
|
||||||
{"name": "queue"},
|
{"name": "queue"},
|
||||||
]
|
]
|
||||||
app = FastAPI(openapi_tags=tags_metadata)
|
api_app = FastAPI(openapi_tags=tags_metadata, title="Dashdio API")
|
||||||
player = MusicPlayer()
|
player = MusicPlayer()
|
||||||
dl_service = DownloadService()
|
dl_service = DownloadService()
|
||||||
ws_manager = ConnectionManager()
|
ws_manager = ConnectionManager()
|
||||||
|
|
||||||
|
app = FastAPI(title="Dashdio App")
|
||||||
|
|
||||||
# Interface
|
# Interface
|
||||||
@app.get("/", response_class=HTMLResponse)
|
app.mount("/api", api_app)
|
||||||
async def root():
|
app.mount("/", StaticFiles(directory="ui", html=True), name="ui")
|
||||||
with open("index.html") as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
@ -98,7 +45,7 @@ async def queue_broadcast_loop():
|
||||||
|
|
||||||
|
|
||||||
# Status updates
|
# Status updates
|
||||||
@app.websocket("/player")
|
@api_app.websocket("/player")
|
||||||
async def websocket_player(websocket: WebSocket):
|
async def websocket_player(websocket: WebSocket):
|
||||||
await ws_manager.connect(websocket, WSConnectionType.state)
|
await ws_manager.connect(websocket, WSConnectionType.state)
|
||||||
try:
|
try:
|
||||||
|
@ -110,7 +57,7 @@ async def websocket_player(websocket: WebSocket):
|
||||||
|
|
||||||
|
|
||||||
# Queue updates
|
# Queue updates
|
||||||
@app.websocket("/queue")
|
@api_app.websocket("/queue")
|
||||||
async def websocket_queue(websocket: WebSocket):
|
async def websocket_queue(websocket: WebSocket):
|
||||||
await ws_manager.connect(websocket, WSConnectionType.queue)
|
await ws_manager.connect(websocket, WSConnectionType.queue)
|
||||||
try:
|
try:
|
||||||
|
@ -121,43 +68,48 @@ async def websocket_queue(websocket: WebSocket):
|
||||||
ws_manager.disconnect(websocket)
|
ws_manager.disconnect(websocket)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/queue", tags=["queue"])
|
@api_app.get("/queue", tags=["queue"])
|
||||||
def get_queue():
|
def get_queue():
|
||||||
return player.get_queue()
|
return player.get_queue()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/queue", tags=["queue"])
|
@api_app.post("/queue", tags=["queue"])
|
||||||
async def post_to_queue(url: str):
|
async def post_to_queue(url: str):
|
||||||
track = await dl_service.download(url)
|
track = await dl_service.download(url)
|
||||||
await player.add_to_queue(track)
|
await player.add_to_queue(track)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/player/play", tags=["player"])
|
@api_app.delete("/queue/{track_id}", tags=["queue"])
|
||||||
|
def delete_from_queue(track_id: str):
|
||||||
|
player.remove_from_queue_by_id(track_id)
|
||||||
|
|
||||||
|
|
||||||
|
@api_app.post("/player/play", tags=["player"])
|
||||||
async def player_play():
|
async def player_play():
|
||||||
await player.play()
|
await player.play()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/player/stop", tags=["player"])
|
@api_app.post("/player/stop", tags=["player"])
|
||||||
async def player_stop():
|
async def player_stop():
|
||||||
await player.stop()
|
await player.stop()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/player/skip", tags=["player"])
|
@api_app.post("/player/skip", tags=["player"])
|
||||||
async def player_skip():
|
async def player_skip():
|
||||||
await player.next()
|
await player.next()
|
||||||
|
|
||||||
|
|
||||||
# Player
|
# Player
|
||||||
@app.put("/player/volume", tags=["player"])
|
@api_app.put("/player/volume", tags=["player"])
|
||||||
async def set_volume(volume: float):
|
async def set_volume(volume: float):
|
||||||
await player.set_volume(volume)
|
await player.set_volume(volume)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/player/volume", tags=["player"])
|
@api_app.get("/player/volume", tags=["player"])
|
||||||
def get_volume():
|
def get_volume():
|
||||||
return {"volume": player.get_volume()}
|
return {"volume": player.get_volume()}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/player", tags=["player"])
|
@api_app.get("/player", tags=["player"])
|
||||||
def get_player_state() -> PlayerState:
|
def get_player_state() -> PlayerState:
|
||||||
return player.get_state()
|
return player.get_state()
|
||||||
|
|
|
@ -3,18 +3,25 @@ import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class Track(BaseModel):
|
class Track(BaseModel):
|
||||||
|
id: str = str(uuid4())
|
||||||
artist: str | None
|
artist: str | None
|
||||||
title: str | None
|
title: str | None
|
||||||
duration: int | None
|
duration: int | None
|
||||||
filepath: str
|
filepath: str
|
||||||
|
|
||||||
|
|
||||||
|
class WSConnectionType(Enum):
|
||||||
|
state = "state"
|
||||||
|
queue = "queue"
|
||||||
|
|
||||||
|
|
||||||
class PlaybackState(str, Enum):
|
class PlaybackState(str, Enum):
|
||||||
Playing = "Playing"
|
Playing = "Playing"
|
||||||
Paused = "Paused"
|
Paused = "Paused"
|
||||||
|
@ -40,6 +47,14 @@ class Queue(BaseModel):
|
||||||
def add(self, track: Track) -> None:
|
def add(self, track: Track) -> None:
|
||||||
self.items.append(track)
|
self.items.append(track)
|
||||||
|
|
||||||
|
def remove_by_id(self, track_id: str) -> None:
|
||||||
|
track = self.get_by_id(track_id)
|
||||||
|
if track:
|
||||||
|
self.items.remove(track)
|
||||||
|
|
||||||
|
def get_by_id(self, track_id: str) -> Track | None:
|
||||||
|
return next((x for x in self.items if x.id == track_id), None)
|
||||||
|
|
||||||
def len(self) -> int:
|
def len(self) -> int:
|
||||||
return len(self.items)
|
return len(self.items)
|
||||||
|
|
||||||
|
@ -111,7 +126,8 @@ class MusicPlayer:
|
||||||
async def _unload_track(self) -> None:
|
async def _unload_track(self) -> None:
|
||||||
pygame.mixer.music.unload()
|
pygame.mixer.music.unload()
|
||||||
# Delete file from disc
|
# Delete file from disc
|
||||||
os.remove(self._state.track.filepath)
|
if self._state.track:
|
||||||
|
os.remove(self._state.track.filepath)
|
||||||
# Update state
|
# Update state
|
||||||
await self._set_track(None)
|
await self._set_track(None)
|
||||||
|
|
||||||
|
@ -162,6 +178,11 @@ class MusicPlayer:
|
||||||
if que_len == 0 and not self._state.track:
|
if que_len == 0 and not self._state.track:
|
||||||
await self._play_next_track()
|
await self._play_next_track()
|
||||||
|
|
||||||
|
def remove_from_queue_by_id(self, track_id: str):
|
||||||
|
with self.lock:
|
||||||
|
self._queue.remove_by_id(track_id)
|
||||||
|
self._queue_event.set()
|
||||||
|
|
||||||
async def play(self):
|
async def play(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
match self._state.playback_state:
|
match self._state.playback_state:
|
13
py-dj.service
Normal file
13
py-dj.service
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Start py-dj FastAPI service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=pi
|
||||||
|
WorkingDirectory=/home/pi/py-dj
|
||||||
|
ExecStart=/home/pi/py-dj/start-api.sh
|
||||||
|
Restart=on-failure
|
||||||
|
Environment=PATH=/home/pi/.local/bin:/usr/bin:/bin
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -6,11 +6,9 @@ readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi[standard]>=0.115.12",
|
"fastapi[standard]>=0.115.12",
|
||||||
"pandas>=2.2.3",
|
|
||||||
"pydantic>=2.11.3",
|
"pydantic>=2.11.3",
|
||||||
"pydub>=0.25.1",
|
"pydub>=0.25.1",
|
||||||
"pygame>=2.6.1",
|
"pygame>=2.6.1",
|
||||||
"websockets >= 15.0.1",
|
"websockets >= 15.0.1",
|
||||||
"yt-dlp>=2025.3.31",
|
"yt-dlp>=2025.3.31",
|
||||||
"ytmusicapi>=1.10.3",
|
|
||||||
]
|
]
|
||||||
|
|
83
services.py
Normal file
83
services.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import yt_dlp
|
||||||
|
from fastapi import WebSocket
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from models import Track, WSConnectionType
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionManager:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.active_connections: dict[str, set[WebSocket]] = {
|
||||||
|
WSConnectionType.state.value: set(),
|
||||||
|
WSConnectionType.queue.value: set(),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket, type: WSConnectionType):
|
||||||
|
await websocket.accept()
|
||||||
|
self.active_connections[type.value].add(websocket)
|
||||||
|
|
||||||
|
async def send(self, ws: WebSocket, message: BaseModel):
|
||||||
|
try:
|
||||||
|
await ws.send_json(message.model_dump())
|
||||||
|
except Exception:
|
||||||
|
self.disconnect(ws)
|
||||||
|
|
||||||
|
async def broadcast(self, ws_type: WSConnectionType, message: BaseModel):
|
||||||
|
broken = set()
|
||||||
|
conn_list = list(self.active_connections[ws_type.value])
|
||||||
|
for idx in range(len(conn_list)):
|
||||||
|
ws = conn_list[idx]
|
||||||
|
try:
|
||||||
|
await ws.send_json(message.model_dump())
|
||||||
|
except Exception:
|
||||||
|
broken.add(ws)
|
||||||
|
for ws in broken:
|
||||||
|
self.disconnect(ws)
|
||||||
|
|
||||||
|
def disconnect(self, websocket: WebSocket):
|
||||||
|
if websocket in self.active_connections[WSConnectionType.state.value]:
|
||||||
|
self.active_connections[WSConnectionType.state.value].remove(websocket)
|
||||||
|
|
||||||
|
if websocket in self.active_connections[WSConnectionType.queue.value]:
|
||||||
|
self.active_connections[WSConnectionType.queue.value].remove(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadService:
|
||||||
|
ydl_opts = {
|
||||||
|
"format": "mp3/bestaudio/best",
|
||||||
|
# ℹ️ See help(yt_dlp.postprocessor) for a list of available Postprocessors and their arguments
|
||||||
|
"postprocessors": [
|
||||||
|
{
|
||||||
|
"key": "FFmpegExtractAudio",
|
||||||
|
"preferredcodec": "mp3",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {"home": "queue"},
|
||||||
|
"outtmpl": {"default": "%(artist)s - %(track)s [%(id)s].%(ext)s"},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.ydl = yt_dlp.YoutubeDL(self.ydl_opts)
|
||||||
|
|
||||||
|
async def download(self, url: str) -> Track:
|
||||||
|
def extract():
|
||||||
|
return self.ydl.extract_info(url, download=True)
|
||||||
|
|
||||||
|
info = await asyncio.to_thread(extract)
|
||||||
|
|
||||||
|
try:
|
||||||
|
filepath = info["requested_downloads"][-1]["filepath"] # type: ignore
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError("Could not ")
|
||||||
|
track = Track(
|
||||||
|
artist=info.get("artist", None), # type: ignore
|
||||||
|
title=info.get("title", None), # type: ignore
|
||||||
|
duration=info.get("duration", None), # type: ignore
|
||||||
|
filepath=filepath, # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Finished processing {track}")
|
||||||
|
|
||||||
|
return track
|
14
start-api.sh
Executable file
14
start-api.sh
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd /home/pi/py-dj
|
||||||
|
|
||||||
|
# Pull latest changes
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Clear the queue directory
|
||||||
|
rm -rf queue/*
|
||||||
|
mkdir -p queue # Ensure it exists
|
||||||
|
|
||||||
|
# Start the API
|
||||||
|
uv run fastapi run
|
56
ui/index.html
Normal file
56
ui/index.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Music Player</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; padding: 1rem; }
|
||||||
|
button { margin: 0.5rem; }
|
||||||
|
#queueList { margin-top: 1rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="font-family: sans-serif; max-width: 600px; margin: auto;">
|
||||||
|
<h2>🎵 Dashdio</h2>
|
||||||
|
<div id="nowPlaying">
|
||||||
|
<strong>Artist:</strong> <span id="trackArtist">-</span><br>
|
||||||
|
<strong>Title:</strong> <span id="trackTitle">-</span><br>
|
||||||
|
<strong>Status:</strong> <span id="playbackState">-</span><br>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<progress id="progressBar" value="0" max="100" style="width: 100%; height: 20px;"></progress>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<span id="elapsedTime">0:00</span> / <span id="totalTime">0:00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<label for="volumeSlider"><strong>🔊 Volume:</strong> <span id="volumeValue">1</span></label><br>
|
||||||
|
<input type="range" min="0" max="1" step="0.01" id="volumeSlider" oninput="setVolume(this.value)">
|
||||||
|
<button onclick="play()">Play/Pause</button>
|
||||||
|
<button onclick="stop()">Stop</button>
|
||||||
|
<button onclick="skip()">Skip</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top: 30px;">📃 Queue</h3>
|
||||||
|
<input type="text" id="trackUrl" placeholder="Track URL">
|
||||||
|
<button onclick="addToQueue()">Add to Queue</button>
|
||||||
|
<table id="queueTable" style="width: 100%; border-collapse: collapse; font-size: 0.95em;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f0f0f0;">
|
||||||
|
<th style="text-align: left; padding: 8px;">#</th>
|
||||||
|
<th style="text-align: left; padding: 8px;">Artist</th>
|
||||||
|
<th style="text-align: left; padding: 8px;">Title</th>
|
||||||
|
<th style="text-align: right; padding: 8px;">Duration</th>
|
||||||
|
<th style="text-align: center; padding: 8px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="queueBody">
|
||||||
|
<!-- Filled by JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
102
ui/script.js
Normal file
102
ui/script.js
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
|
||||||
|
function formatTime(seconds) {
|
||||||
|
const m = Math.floor(seconds / 60);
|
||||||
|
const s = Math.floor(seconds % 60).toString().padStart(2, '0');
|
||||||
|
return `${m}:${s}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFromQueue = async (trackId) => {
|
||||||
|
await api(`/api/queue/${trackId}`, { method: "DELETE" });
|
||||||
|
updateQueue(); // Refresh queue after deletion
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function updateProgress(position, duration) {
|
||||||
|
const progressBar = document.getElementById("progressBar");
|
||||||
|
const elapsedTime = document.getElementById("elapsedTime");
|
||||||
|
const totalTime = document.getElementById("totalTime");
|
||||||
|
|
||||||
|
progressBar.max = duration;
|
||||||
|
progressBar.value = position;
|
||||||
|
elapsedTime.textContent = formatTime(position);
|
||||||
|
totalTime.textContent = formatTime(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const api = (endpoint, options = {}) =>
|
||||||
|
fetch(endpoint, options).then(res => res.json()).catch(console.error);
|
||||||
|
|
||||||
|
const setVolume = async (val) => {
|
||||||
|
await api(`/api/player/volume?volume=${val}`, { method: "PUT" });
|
||||||
|
document.getElementById("volumeValue").textContent = val;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToQueue = async () => {
|
||||||
|
const url = document.getElementById("trackUrl").value;
|
||||||
|
await api(`/api/queue?url=${encodeURIComponent(url)}`, { method: "POST" });
|
||||||
|
updateQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
const play = async () => { await api("/api/player/play", { method: "POST" });};
|
||||||
|
const stop = async () => { await api("/api/player/stop", { method: "POST" });};
|
||||||
|
const skip = async () => { await api("/api/player/skip", { method: "POST" });};
|
||||||
|
|
||||||
|
// WebSocket connections
|
||||||
|
let playerSocket, queueSocket;
|
||||||
|
|
||||||
|
function connectWebSockets() {
|
||||||
|
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||||
|
const base = `${proto}://${location.host}/api`;
|
||||||
|
|
||||||
|
playerSocket = new WebSocket(`${base}/player`);
|
||||||
|
queueSocket = new WebSocket(`${base}/queue`);
|
||||||
|
|
||||||
|
playerSocket.onopen = () => playerSocket.send("ping");
|
||||||
|
queueSocket.onopen = () => queueSocket.send("ping");
|
||||||
|
|
||||||
|
playerSocket.onmessage = (event) => {
|
||||||
|
const state = JSON.parse(event.data);
|
||||||
|
|
||||||
|
const { playback_state, track, position, volume } = state;
|
||||||
|
|
||||||
|
document.getElementById("trackArtist").textContent = track?.artist || "-";
|
||||||
|
document.getElementById("trackTitle").textContent = track?.title || "-";
|
||||||
|
document.getElementById("playbackState").textContent = playback_state;
|
||||||
|
|
||||||
|
document.getElementById("volumeSlider").value = volume;
|
||||||
|
document.getElementById("volumeValue").textContent = volume;
|
||||||
|
|
||||||
|
if (track) {
|
||||||
|
updateProgress(position, track.duration);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
queueSocket.onmessage = (event) => {
|
||||||
|
const queue = JSON.parse(event.data);
|
||||||
|
const queueBody = document.getElementById("queueBody");
|
||||||
|
queueBody.innerHTML = "";
|
||||||
|
|
||||||
|
(queue.items || queue).forEach((track, index) => {
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td style="padding: 8px;">${index + 1}</td>
|
||||||
|
<td style="padding: 8px;">${track.artist}</td>
|
||||||
|
<td style="padding: 8px;">${track.title}</td>
|
||||||
|
<td style="padding: 8px; text-align: right;">${formatTime(track.duration)}</td>
|
||||||
|
<td style="padding: 8px; text-align: center;">
|
||||||
|
<button onclick="removeFromQueue('${track.id}')">🗑️</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
queueBody.appendChild(row);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
playerSocket.onerror = queueSocket.onerror = console.error;
|
||||||
|
playerSocket.onclose = () => setTimeout(connectWebSockets, 1000);
|
||||||
|
queueSocket.onclose = () => setTimeout(connectWebSockets, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectWebSockets();
|
||||||
|
|
156
uv.lock
generated
156
uv.lock
generated
|
@ -33,28 +33,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
|
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "charset-normalizer"
|
|
||||||
version = "3.4.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.8"
|
version = "8.1.8"
|
||||||
|
@ -263,86 +241,27 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "numpy"
|
|
||||||
version = "2.2.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pandas"
|
|
||||||
version = "2.2.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "numpy" },
|
|
||||||
{ name = "python-dateutil" },
|
|
||||||
{ name = "pytz" },
|
|
||||||
{ name = "tzdata" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "py-dj"
|
name = "py-dj"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastapi", extra = ["standard"] },
|
{ name = "fastapi", extra = ["standard"] },
|
||||||
{ name = "pandas" },
|
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydub" },
|
{ name = "pydub" },
|
||||||
{ name = "pygame" },
|
{ name = "pygame" },
|
||||||
{ name = "websockets" },
|
{ name = "websockets" },
|
||||||
{ name = "yt-dlp" },
|
{ name = "yt-dlp" },
|
||||||
{ name = "ytmusicapi" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
|
||||||
{ name = "pandas", specifier = ">=2.2.3" },
|
|
||||||
{ name = "pydantic", specifier = ">=2.11.3" },
|
{ name = "pydantic", specifier = ">=2.11.3" },
|
||||||
{ name = "pydub", specifier = ">=0.25.1" },
|
{ name = "pydub", specifier = ">=0.25.1" },
|
||||||
{ name = "pygame", specifier = ">=2.6.1" },
|
{ name = "pygame", specifier = ">=2.6.1" },
|
||||||
{ name = "websockets", specifier = ">=15.0.1" },
|
{ name = "websockets", specifier = ">=15.0.1" },
|
||||||
{ name = "yt-dlp", specifier = ">=2025.3.31" },
|
{ name = "yt-dlp", specifier = ">=2025.3.31" },
|
||||||
{ name = "ytmusicapi", specifier = ">=1.10.3" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -421,18 +340,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-dateutil"
|
|
||||||
version = "2.9.0.post0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "six" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -451,15 +358,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
|
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytz"
|
|
||||||
version = "2025.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.2"
|
version = "6.0.2"
|
||||||
|
@ -477,21 +375,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "requests"
|
|
||||||
version = "2.32.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "charset-normalizer" },
|
|
||||||
{ name = "idna" },
|
|
||||||
{ name = "urllib3" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "14.0.0"
|
version = "14.0.0"
|
||||||
|
@ -528,15 +411,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "six"
|
|
||||||
version = "1.17.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
@ -594,24 +468,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
|
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tzdata"
|
|
||||||
version = "2025.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "urllib3"
|
|
||||||
version = "2.4.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.34.0"
|
version = "0.34.0"
|
||||||
|
@ -701,15 +557,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/a2/11/333d16f88b1515d4c
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/bf/7b0affb8f163376309696cfd1c677818fa0969fbb9d88225087208799afe/yt_dlp-2025.3.31-py3-none-any.whl", hash = "sha256:8ecb3aa218a3bebe431119f513a8972b9b9d992edf67168c00ab92329a03baec", size = 3226021 },
|
{ url = "https://files.pythonhosted.org/packages/a8/bf/7b0affb8f163376309696cfd1c677818fa0969fbb9d88225087208799afe/yt_dlp-2025.3.31-py3-none-any.whl", hash = "sha256:8ecb3aa218a3bebe431119f513a8972b9b9d992edf67168c00ab92329a03baec", size = 3226021 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ytmusicapi"
|
|
||||||
version = "1.10.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "requests" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/87/2e/30b77a0aec8383a5d3b1472e763ff14f845c275c103da78d3c0045cd691a/ytmusicapi-1.10.3.tar.gz", hash = "sha256:7235361ac9d5958d49a29f586eec55b1c83e90f7b063361e8a206e1cf4f76216", size = 311765 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/2c/cba9e462497adbf5960fe74fdc5d7bb44153f0037264f3dea7653e5d4969/ytmusicapi-1.10.3-py3-none-any.whl", hash = "sha256:07059d6a60d824636edc4032c30603f45f8072961821c6159e33d61cd02fc451", size = 93484 },
|
|
||||||
]
|
|
||||||
|
|
28
ytmusic.py
28
ytmusic.py
|
@ -1,28 +0,0 @@
|
||||||
import pandas as pd
|
|
||||||
from ytmusicapi import YTMusic
|
|
||||||
|
|
||||||
ytmusic = YTMusic()
|
|
||||||
|
|
||||||
search_string = input("Input the search string: ")
|
|
||||||
search_results = ytmusic.search(search_string)
|
|
||||||
|
|
||||||
df = pd.json_normalize(search_results)
|
|
||||||
|
|
||||||
df = df[
|
|
||||||
df["category"].isin(["Top result", "Songs", "Videos"])
|
|
||||||
& df["resultType"].isin(["song", "video"])
|
|
||||||
]
|
|
||||||
|
|
||||||
df = df[
|
|
||||||
[
|
|
||||||
"category",
|
|
||||||
"resultType",
|
|
||||||
"videoId",
|
|
||||||
"title",
|
|
||||||
"artists",
|
|
||||||
"views",
|
|
||||||
"duration_seconds",
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
print(df)
|
|
Loading…
Add table
Add a link
Reference in a new issue