Files
Leonid PershinandClaude Opus 5 ffcf88efb7 Translate yt-dlp errors and drop the redundant prefix hint
Errors reached people as raw tool output: "Unsupported URL:
https://...%D0%B0%D1%83..." says nothing to whoever pasted a link. The
common cases now map to plain descriptions with the next step where there
is one (age gate and bot checks point at YTDLP_COOKIES, a stale extractor
at updating yt-dlp), URLs are decoded, and anything unrecognised still
falls through verbatim.

The panel's prefix hint is gone now that the source picker does the same
job visibly; prefixes keep working and stay documented for chat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:15:25 +03:00

133 lines
4.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, type FormEvent } from "react";
import { api, formatLength } from "../api";
import type { Track } from "../types";
const SOURCE_BADGE: Record<Track["source"], string> = {
youtube: "YouTube",
soundcloud: "SoundCloud",
direct: "Ссылка",
local: "Медиатека",
};
interface Props {
serverId: string;
canControl: boolean;
/** Adds the local library to the source picker. */
localLibrary: boolean;
onError(message: string | null): void;
}
const SOURCE_KEY = "mbot.searchSource";
export function SearchPanel({ serverId, canControl, localLibrary, onError }: Props) {
const [query, setQuery] = useState("");
const [source, setSource] = useState<string>(() => localStorage.getItem(SOURCE_KEY) ?? "all");
const [results, setResults] = useState<Track[]>([]);
const [busy, setBusy] = useState(false);
const [lastAdded, setLastAdded] = useState<string | null>(null);
const [searched, setSearched] = useState(false);
async function submit(event: FormEvent) {
event.preventDefault();
const value = query.trim();
if (!value) return;
setBusy(true);
onError(null);
try {
if (/^https?:\/\//i.test(value)) {
await api.play(serverId, { query: value });
setLastAdded(value);
setResults([]);
setSearched(false);
setQuery("");
return;
}
const { tracks } = await api.search(serverId, value, source);
setResults(tracks);
setSearched(true);
} catch (err) {
onError(err instanceof Error ? err.message : "Поиск не удался");
} finally {
setBusy(false);
}
}
async function enqueue(track: Track, mode: "append" | "next" | "now") {
onError(null);
try {
await api.play(serverId, { trackIds: [track.id], mode });
setLastAdded(track.title);
} catch (err) {
onError(err instanceof Error ? err.message : "Не удалось добавить трек");
}
}
return (
<div className="card grow">
<h2>Поиск</h2>
<form className="search-form" onSubmit={submit}>
<select
value={source}
onChange={(event) => {
setSource(event.target.value);
localStorage.setItem(SOURCE_KEY, event.target.value);
}}
disabled={!canControl}
title="Где искать"
>
<option value="all">Везде</option>
<option value="youtube">YouTube</option>
<option value="soundcloud">SoundCloud</option>
{localLibrary && <option value="local">Медиатека</option>}
</select>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Название трека или ссылка…"
disabled={!canControl}
/>
<button className="primary" type="submit" disabled={busy || !canControl}>
{busy ? "…" : "Найти"}
</button>
</form>
{results.length === 0 ? (
<div className="empty">
{searched
? `По запросу «${query}» ничего не нашлось. YouTube иногда прячет результаты от ботов — попробуйте другие слова или вставьте прямую ссылку на трек.`
: lastAdded
? `Добавлено: ${lastAdded}`
: "Введите запрос или вставьте ссылку — плейлисты тоже поддерживаются."}
</div>
) : (
<ul className="track-list">
{results.map((track, index) => (
<li className="track" key={track.id}>
<span className="idx">{index + 1}</span>
{track.thumbnail ? <img className="thumb" src={track.thumbnail} alt="" /> : <div className="thumb" />}
<div className="info">
<div className="title">{track.title}</div>
<div className="sub">
{[track.author, formatLength(track)].filter(Boolean).join(" · ")}
</div>
</div>
<span className="badge">{SOURCE_BADGE[track.source]}</span>
<div className="actions">
<button onClick={() => enqueue(track, "now")} disabled={!canControl} title="Играть сейчас">
</button>
<button onClick={() => enqueue(track, "next")} disabled={!canControl} title="Следующим">
</button>
<button onClick={() => enqueue(track, "append")} disabled={!canControl} title="В очередь">
</button>
</div>
</li>
))}
</ul>
)}
</div>
);
}