Добавил бот для Telegram
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package tgbot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
|
||||
"git.vakhrushev.me/av/jellybit/internal/store"
|
||||
"git.vakhrushev.me/av/jellybit/internal/worker"
|
||||
)
|
||||
|
||||
// renderCard строит текст и клавиатуру карточки по состоянию задачи.
|
||||
func (b *Bot) renderCard(rd *worker.ReviewData) (string, *tgbotapi.InlineKeyboardMarkup) {
|
||||
id := rd.Download.ID
|
||||
state := rd.Download.State
|
||||
|
||||
switch state {
|
||||
case store.StateReview, store.StateDeferred:
|
||||
return b.reviewCard(rd)
|
||||
case store.StateRecognizing:
|
||||
return "⏳ Распознаю #" + itoa(id) + "…", b.webOnly(id)
|
||||
case store.StateLinking:
|
||||
return "⏳ Раскладываю #" + itoa(id) + "…", nil
|
||||
case store.StateDone:
|
||||
return b.renderDone(rd), b.webOnly(id)
|
||||
default:
|
||||
text := fmt.Sprintf("Задача #%d — %s.", id, state)
|
||||
if msg := rd.Download.ErrorMsg.String; msg != "" {
|
||||
text += "\n" + msg
|
||||
}
|
||||
return text, b.webOnly(id)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) reviewCard(rd *worker.ReviewData) (string, *tgbotapi.InlineKeyboardMarkup) {
|
||||
id := rd.Download.ID
|
||||
var sb strings.Builder
|
||||
|
||||
fmt.Fprintf(&sb, "🟡 Нужно подтверждение #%d\n", id)
|
||||
if src := contextOrSource(rd); src != "" {
|
||||
fmt.Fprintf(&sb, "Источник: %s\n", shorten(src, 80))
|
||||
}
|
||||
fmt.Fprintf(&sb, "Похоже на: %s\n", guessLine(rd))
|
||||
if base := baseLine(rd.Recognition); base != "" {
|
||||
fmt.Fprintf(&sb, "База: %s\n", base)
|
||||
}
|
||||
if reasons := rd.Recognition.ReasonList(); len(reasons) > 0 {
|
||||
fmt.Fprintf(&sb, "Причины: %s\n", strings.Join(reasons, " · "))
|
||||
}
|
||||
if n := len(rd.Preview); n > 0 {
|
||||
fmt.Fprintf(&sb, "План: %d файлов → %s", n, tailPath(rd.Preview[0].Dst))
|
||||
}
|
||||
|
||||
return strings.TrimRight(sb.String(), "\n"), b.reviewKeyboard(rd)
|
||||
}
|
||||
|
||||
func (b *Bot) reviewKeyboard(rd *worker.ReviewData) *tgbotapi.InlineKeyboardMarkup {
|
||||
id := rd.Download.ID
|
||||
sid := itoa(id)
|
||||
|
||||
var row1 []tgbotapi.InlineKeyboardButton
|
||||
if len(rd.Preview) > 0 {
|
||||
row1 = append(row1, tgbotapi.NewInlineKeyboardButtonData("✅ Применить", "apply:"+sid))
|
||||
}
|
||||
row1 = append(row1, tgbotapi.NewInlineKeyboardButtonData("📺↔🎬 Тип", "type:"+sid+":"+oppositeType(string(rd.Plan.Type))))
|
||||
|
||||
row2 := tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("🔁 Уточнить", "refine:"+sid),
|
||||
tgbotapi.NewInlineKeyboardButtonData("🕗 Позже", "defer:"+sid),
|
||||
)
|
||||
|
||||
var row3 []tgbotapi.InlineKeyboardButton
|
||||
if url := b.reviewURL(id); url != "" {
|
||||
row3 = append(row3, tgbotapi.NewInlineKeyboardButtonURL("🌐 В вебе", url))
|
||||
}
|
||||
row3 = append(row3, tgbotapi.NewInlineKeyboardButtonData("❌ Отклонить", "reject:"+sid))
|
||||
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(row1, row2, row3)
|
||||
return &kb
|
||||
}
|
||||
|
||||
// renderDone — короткое сообщение о готовности.
|
||||
func (b *Bot) renderDone(rd *worker.ReviewData) string {
|
||||
title := rd.Plan.Title
|
||||
if title == "" {
|
||||
title = "#" + itoa(rd.Download.ID)
|
||||
}
|
||||
n := len(rd.Preview)
|
||||
if n == 0 {
|
||||
return fmt.Sprintf("✅ Готово: «%s» разложен.", title)
|
||||
}
|
||||
return fmt.Sprintf("✅ Готово: «%s» — разложено файлов: %d.", title, n)
|
||||
}
|
||||
|
||||
func (b *Bot) webOnly(id int64) *tgbotapi.InlineKeyboardMarkup {
|
||||
url := b.reviewURL(id)
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonURL("🌐 Открыть в вебе", url),
|
||||
))
|
||||
return &kb
|
||||
}
|
||||
|
||||
func (b *Bot) reviewURL(id int64) string {
|
||||
if b.webBase == "" {
|
||||
return ""
|
||||
}
|
||||
return b.webBase + "/review/" + itoa(id)
|
||||
}
|
||||
|
||||
// --- мелкие хелперы ---
|
||||
|
||||
func guessLine(rd *worker.ReviewData) string {
|
||||
emoji, kind := "🎬", "фильм"
|
||||
if rd.Plan.Type == "series" {
|
||||
emoji, kind = "📺", "сериал"
|
||||
}
|
||||
title := rd.Plan.Title
|
||||
if title == "" {
|
||||
title = "не распознано"
|
||||
}
|
||||
s := fmt.Sprintf("%s %s «%s»", emoji, kind, title)
|
||||
if rd.Plan.Year != 0 {
|
||||
s += fmt.Sprintf(" (%d)", rd.Plan.Year)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func baseLine(rec *store.Recognition) string {
|
||||
if rec == nil || !rec.Provider.Valid || rec.Provider.String == "" || rec.Provider.String == "none" {
|
||||
return "нет матча"
|
||||
}
|
||||
if rec.ProviderID.Valid && rec.ProviderID.String != "" {
|
||||
return rec.Provider.String + " " + rec.ProviderID.String
|
||||
}
|
||||
return rec.Provider.String
|
||||
}
|
||||
|
||||
func contextOrSource(rd *worker.ReviewData) string {
|
||||
if c := strings.TrimSpace(rd.Download.Context); c != "" {
|
||||
return firstLine(c)
|
||||
}
|
||||
return rd.Download.SourceRef
|
||||
}
|
||||
|
||||
func oppositeType(t string) string {
|
||||
if t == "series" {
|
||||
return "movie"
|
||||
}
|
||||
return "series"
|
||||
}
|
||||
|
||||
func firstLine(s string) string {
|
||||
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func tailPath(p string) string {
|
||||
dir, file := filepath.Split(p)
|
||||
parent := filepath.Base(strings.TrimRight(dir, "/"))
|
||||
if parent == "." || parent == "/" || parent == "" {
|
||||
return file
|
||||
}
|
||||
return parent + "/" + file
|
||||
}
|
||||
|
||||
func shorten(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= n {
|
||||
return s
|
||||
}
|
||||
return string(r[:n]) + "…"
|
||||
}
|
||||
|
||||
func itoa(n int64) string { return strconv.FormatInt(n, 10) }
|
||||
Reference in New Issue
Block a user