Skip to content
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b3b51fa
ISSUE-67: Add AI slop detection and auto-skip functionality
vtvz Feb 25, 2026
29a5243
prefix
vtvz Feb 26, 2026
6d1c873
links
vtvz Feb 26, 2026
2022207
ISSUE-67: Add AI slop notification UI with artist page button
vtvz Feb 26, 2026
268d94c
ISSUE-67: Add soul-over-ai provider for AI slop detection
vtvz Feb 26, 2026
9457cca
ISSUE-67: Add spot-the-ai provider for AI slop detection
vtvz Feb 27, 2026
1daa0e6
ISSUE-67: Add error handling for AI slop detection providers
vtvz Feb 27, 2026
a322d0b
ISSUE-67: Add SHLabs provider for AI slop detection
vtvz Feb 27, 2026
7af0f3a
ISSUE-67: Add provider attribution to AI slop notifications
vtvz Feb 28, 2026
b98322a
ISSUE-67: Document AI-generated music detection in README
vtvz Feb 28, 2026
3c286cb
ISSUE-67: Add user configuration for AI slop detection behavior
vtvz Mar 2, 2026
a08c962
ISSUE-67: Add localization and UI for AI slop detection settings
vtvz Mar 2, 2026
2589fab
ISSUE-67: Refactor AI slop detection logic and improve configuration
vtvz Mar 3, 2026
728d87a
ISSUE-67: Rename REDIS_KEY_ARTIST_PREFIX to REDIS_KEY_TRACK_PREFIX
vtvz Mar 3, 2026
de17cdf
fix
vtvz Mar 4, 2026
976adda
added missing
vtvz Mar 6, 2026
db22ea7
ISSUE-67: Add AISlopPredict trait and prediction type classification
vtvz Mar 6, 2026
4d6d58b
ISSUE-67: Rename AISlopPredict trait and improve button labels
vtvz Mar 7, 2026
fde8ad2
ISSUE-67: Fix TOCTOU race in AI slop detection providers
vtvz Mar 7, 2026
abb4062
ISSUE-67: Add tracing instrumentation to AI slop detection
vtvz Mar 7, 2026
1b869ea
fix skipped
vtvz Mar 7, 2026
be0a47f
ISSUE-67: Fix atomic flag not reset on Redis write failure
vtvz Mar 7, 2026
ac8c93b
unreachable
vtvz Mar 7, 2026
6ca7d6b
ISSUE-67: Optimize Redis writes and add auth check
vtvz Mar 7, 2026
2bc735e
ISSUE-67: Remove SpotTheAI provider
vtvz Mar 8, 2026
cd271d6
ISSUE-67: Use dynamic button labels in action messages
vtvz Mar 8, 2026
fc507c4
ISSUE-67: Skip AI detection for tracks released before 2024
vtvz Mar 8, 2026
1d044d3
ISSUE-67: Improve AI slop alert message wording
vtvz Mar 8, 2026
645ee3a
ISSUE-67: Show AI slop config in admin user details
vtvz Mar 8, 2026
7dc8703
ISSUE-67: Move skip counter increment to caller
vtvz Mar 8, 2026
13b030a
ISSUE-67: Log errors when incrementing skip counter
vtvz Mar 8, 2026
b0e76a6
ISSUE-67: Add rate limiting for SHLabs provider
vtvz Mar 8, 2026
479f134
ISSUE-67: Use Instant for populate timeout
vtvz Mar 8, 2026
7bf623f
ISSUE-67: Log actual pause duration in rate limit warning
vtvz Mar 8, 2026
209c11a
ISSUE-67: Use Instant for populate timeout in SoulOverAI
vtvz Mar 8, 2026
e8c4693
ISSUE-67: Remove unused Utc import and use const for hour
vtvz Mar 8, 2026
56fc00b
ISSUE-67: Use const for expiry seconds in SoulOverAI populate
vtvz Mar 8, 2026
6c53b32
ISSUE-67: Use const for expiry seconds in SpotifyAIBlocker
vtvz Mar 8, 2026
3b587c9
ISSUE-67: Separate URL action buttons from callback buttons
vtvz Mar 8, 2026
32617b0
ISSUE-67: Simplify AI slop skip logic for non-premium users
vtvz Mar 8, 2026
3e99ce7
ISSUE-67: Clean up serde imports
vtvz Mar 8, 2026
dc15ea3
ISSUE-67: Check cache before rate limit in SHLabs fetch
vtvz Mar 8, 2026
48e4262
exact text
vtvz Mar 9, 2026
12d8299
ISSUE-67: Extract expiry calculation with explanatory comment
vtvz Mar 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ uuid = "1.20.0"
whatlang = { version = "0.18.0", features = ["serde"] }
redis = "0.32.7"
prometheus = { version = "0.14.0", features = ["push", "process"] }
csv = "1.4.0"
async-trait = "0.1.89"

[dependencies.isolang]
features = ["serde"]
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

> **📢 Subscribe to the author's telegram channel for updates and more projects:** [**@vtvz_dev**](https://t.me/vtvz_dev)

> A Telegram bot that monitors your Spotify playback, detects profane lyrics, integrates with AI for text analysis, and automatically skips tracks you've disliked
> A Telegram bot that monitors your Spotify playback, detects profane lyrics and AI-generated music, integrates with AI for text analysis, and automatically skips tracks you've disliked

[![Rust](https://img.shields.io/badge/rust-nightly-orange.svg)](https://www.rust-lang.org/)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
Expand All @@ -23,13 +23,14 @@

---

Rustify is an intelligent Telegram bot that integrates with Spotify to provide real-time profanity detection and track management. It continuously monitors what you're listening to, analyzes lyrics for inappropriate content, and automatically skips tracks you've marked with the dislike button.
Rustify is an intelligent Telegram bot that integrates with Spotify to provide real-time profanity detection, AI-generated music detection, and track management. It continuously monitors what you're listening to, analyzes lyrics for inappropriate content, detects AI-generated tracks, and automatically skips tracks you've marked with the dislike button.

## ✨ Features

### 🎯 Core Features

- **🔍 Real-time Profanity Detection** - Automatically analyzes song lyrics as you listen using advanced profanity detection algorithms
- **🤖 AI-Generated Music Detection** - Identifies AI-generated tracks using multiple detection providers and shows notifications with attribution
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **⏭️ Auto-Skip** - Instantly skips tracks you've marked with dislike
- **📊 Multi-Provider Lyrics** - Fetches lyrics from multiple sources (Musixmatch, Genius, LrcLib) for maximum coverage
- **🤖 AI-Powered Analysis** - Optional OpenAI-compatible API integration for analyzing song lyrics meaning, storyline, and content themes, plus individual word analysis
Expand Down
8 changes: 4 additions & 4 deletions locales/actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,26 @@ actions.dislike:
en: |-
👎 Disliked %{track_link}

If you change your mind, press 'Ignore text 🙈'
If you change your mind, press '%{ignore_button_label}'

💡 Do not forget you can send a link to a song to find current status
ru: |-
👎 Дизлайкнул %{track_link}

Если передумаете, нажмите 'Игнорировать текст 🙈'
Если передумаете, нажмите '%{ignore_button_label}'

💡 Не забывайте, что можете отправить ссылку на песню, чтобы узнать текущий статус

actions.ignore:
en: |-
🙈 Bad words of %{track_link} will be forever ignored

If you change your mind, press 'Dislike 👎'
If you change your mind, press '%{dislike_button_label}'

💡 Do not forget you can send a link to a song to find current status
ru: |-
🙈 Плохие слова из %{track_link} будут навсегда проигнорированы

Если передумаете, нажмите 'Не нравится 👎'
Если передумаете, нажмите '%{dislike_button_label}'

💡 Не забывайте, что можете отправить ссылку на песню, чтобы узнать текущий статус
75 changes: 75 additions & 0 deletions locales/ai_slop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
_version: 2

ai-slop.alert:
en: |-
🎵 %{track_name}
Album: %{album_name}

<tg-emoji emoji-id="5429334864010681294">💩</tg-emoji> This track is most likely <b>%{prediction}</b>. You can block track or ignore similar notifications for this track

To permanently block this artist from recommendations:
<tg-emoji emoji-id="6028171274939797252">🔗</tg-emoji> Open the artist's page in Spotify
<tg-emoji emoji-id="6032924188828767321">➕</tg-emoji> Tap the three dots near the <i>"Follow"</i> button
<tg-emoji emoji-id="5774077015388852135">❌</tg-emoji> Select <i>"Don't play this artist"</i>

<i>Information provided by "%{ai_check_provider}"</i>

<b>Support real organic hand-made music instead of AI-slop!</b>

<blockquote>Want to disable these alerts or auto-skip AI tracks? Try /%{config_command}</blockquote>
Comment thread
vtvz marked this conversation as resolved.
ru: |-
🎵 %{track_name}
Альбом: %{album_name}

<tg-emoji emoji-id="5429334864010681294">💩</tg-emoji> Этот трек скорее всего <b>%{prediction}</b>. Вы можете заблокировать трек или отключить подобные уведомления для этого трека

Чтобы навсегда скрыть этого исполнителя из рекомендаций:
<tg-emoji emoji-id="6028171274939797252">🔗</tg-emoji> Откройте страницу исполнителя в Spotify
<tg-emoji emoji-id="6032924188828767321">➕</tg-emoji> Нажмите на три точки рядом с кнопкой <i>«Подписаться»</i>
<tg-emoji emoji-id="5774077015388852135">❌</tg-emoji> Выберите <i>«Не включать треки этого исполнителя»</i>

<i>Информация предоставлена "%{ai_check_provider}"</i>

<b>Поддерживайте настоящую живую музыку вместо ИИ-шлака!</b>

<blockquote>Хотите отключить уведомления или настроить автопропуск AI-треков? Попробуйте /%{config_command}</blockquote>

ai-slop.alert-pure-ai:
en: fully AI-generated
ru: полностью сгенерирован ИИ

ai-slop.alert-processed-ai:
en: partially generated or processed by AI
ru: частично сгенерирован или обработан ИИ

ai-slop.setting-description:
en: |-
<tg-emoji emoji-id="5429334864010681294">💩</tg-emoji> AI Music Detection

Bot automatically checks tracks for AI generation

Configure bot's reaction to such tracks:
ru: |-
<tg-emoji emoji-id="5429334864010681294">💩</tg-emoji> Обнаружение ИИ-музыки

Бот автоматически проверяет треки на ИИ-генерацию

Настройте реакцию бота на такие треки:

ai-slop.button-notify:
en: |-
%{mark}Notify
ru: |-
%{mark}Уведомлять

ai-slop.button-ignore:
en: |-
%{mark}Don't notify
ru: |-
%{mark}Не уведомлять

ai-slop.button-skip:
en: |-
%{mark}Skip automatically
ru: |-
%{mark}Пропускать автоматически
14 changes: 10 additions & 4 deletions locales/inline_buttons.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@ _version: 2

inline-buttons.dislike:
en: |-
Dislike 👎
Block track 👎
ru: |-
Не нравится 👎
Заблокировать трек 👎

inline-buttons.ignore:
en: |-
Ignore text 🙈
Hide alerts for this track 🙈
ru: |-
Игнорировать текст 🙈
Скрыть уведомления для этого трека 🙈

inline-buttons.analyze:
en: |-
Analyze text 🔍
ru: |-
Анализировать текст 🔍

inline-buttons.artist-page:
en: |-
Go to artist's page 🔗
ru: |-
Перейти на страницу исполнителя 🔗

inline-buttons.alert-login:
en: |-
You need to login first
Expand Down
2 changes: 2 additions & 0 deletions locales/login.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ login.invite:
<b>Features:</b>
• Block unwanted songs ("Dislike")
• Notifications for profanity in lyrics (English only)
• Detect AI-generated music (notify/ignore/auto-skip)
Comment thread
vtvz marked this conversation as resolved.
• Get song lyrics and AI-powered analysis
• Create <i>Magic Playlist™</i>✨ from your favorite tracks
• <i>Skippage™</i> function will diverse your listening experience
Expand All @@ -48,6 +49,7 @@ login.invite:
<b>Возможности:</b>
• Блокировка нежелательных песен ("Дизлайк")
• Уведомление на нецензурную лексику в тексте (только английский)
• Обнаружение ИИ-музыки (уведомление/игнорирование/авто-пропуск)
• Получение текстов песен и их анализ с помощью ИИ
• Создание <i>Magic Playlist™</i>✨ из любимых треков
• Функция <i>Skippage™</i> разнообразит ваши впечатления от прослушивания
Expand Down
4 changes: 2 additions & 2 deletions locales/profanity_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ profanity-check.message:

<a href="%{lyrics_link}">%{lyrics_link_text}</a>

Press 'Ignore text 🙈' to never see this notification for <b>this song</b> again
Press '%{ignore_button_label}' to never see this notification for <b>this song</b> again
ru: |-
🚨 Текущая песня (%{track_name}) <b>вероятно</b> содержит нецензурные слова:

%{bad_lines}

<a href="%{lyrics_link}">%{lyrics_link_text}</a>

Нажмите 'Игнорировать текст 🙈', чтобы больше не видеть это уведомление для <b>этой песни</b>
Нажмите '%{ignore_button_label}', чтобы больше не видеть это уведомление для <b>этой песни</b>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alter table "user"
add cfg_ai_slop_detection text default 'notify' not null;
Comment thread
vtvz marked this conversation as resolved.
12 changes: 11 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use teloxide::requests::RequesterExt as _;
use crate::metrics::influx::InfluxClient;
use crate::metrics::prometheus::PrometheusClient;
use crate::queue::QueueManager;
use crate::services::{SongLinkService, UserService};
use crate::services::{AISlopDetectionService, SongLinkService, UserService};
use crate::user::UserState;
use crate::{lyrics, profanity, spotify};

Expand All @@ -32,6 +32,7 @@ pub struct App {
dialogue_storage: Arc<TeloxideRedisStorage<Bincode>>,
server_http_address: String,
song_link: SongLinkService,
ai_slop_detection: AISlopDetectionService,
queue_manager: QueueManager,
}

Expand Down Expand Up @@ -67,6 +68,8 @@ struct EnvConfig {
genius_service_url: String,
lyrics_cache_ttl: Option<u64>,

shlabs_api_key: Option<String>,
Comment thread
vtvz marked this conversation as resolved.

censor_blacklist: Option<String>,
censor_whitelist: Option<String>,

Expand Down Expand Up @@ -137,6 +140,10 @@ impl App {
pub fn queue_manager(&self) -> &QueueManager {
&self.queue_manager
}

pub fn ai_slop_detection(&self) -> &AISlopDetectionService {
&self.ai_slop_detection
}
}

fn init_influx(env: &EnvConfig) -> anyhow::Result<Option<InfluxClient>> {
Expand Down Expand Up @@ -338,6 +345,8 @@ impl App {

let queue_manager = QueueManager::new(redis_url).await?;

let shlabs_api_key = env.shlabs_api_key.filter(|key| !key.trim().is_empty());

// Make global static variable to prevent hassle with Arc
let app = Box::new(Self {
bot,
Expand All @@ -354,6 +363,7 @@ impl App {
.server_http_address
.unwrap_or_else(|| "0.0.0.0:3000".into()),
queue_manager,
ai_slop_detection: AISlopDetectionService::new(shlabs_api_key),
});

let app = &*Box::leak(app);
Expand Down
1 change: 1 addition & 0 deletions src/entity/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub use super::track_status::{
};
#[allow(unused_imports)]
pub use super::user::{
AISlopDetection as UserAISlopDetection,
ActiveModel as UserActiveModel,
Column as UserColumn,
Entity as UserEntity,
Expand Down
52 changes: 52 additions & 0 deletions src/entity/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub struct Model {
pub cfg_skip_tracks: bool,
pub cfg_skippage_secs: i64,
pub cfg_skippage_enabled: bool,
#[sea_orm(enum_name = "CfgAISlopDetection")]
pub cfg_ai_slop_detection: AISlopDetection,
pub magic_playlist: Option<String>,
pub spotify_state: Uuid,
pub ref_code: Option<String>,
Expand Down Expand Up @@ -84,6 +86,8 @@ pub enum Column {
CfgSkipTracks,
CfgSkippageSecs,
CfgSkippageEnabled,
#[sea_orm(column_name = "cfg_ai_slop_detection")]
CfgAISlopDetection,
MagicPlaylist,
SpotifyState,
RefCode,
Expand Down Expand Up @@ -126,6 +130,7 @@ impl ColumnTrait for Column {
Self::CfgSkipTracks => ColumnType::Boolean.def(),
Self::CfgSkippageSecs => ColumnType::BigInteger.def(),
Self::CfgSkippageEnabled => ColumnType::Boolean.def(),
Self::CfgAISlopDetection => AISlopDetection::db_type(),
Self::MagicPlaylist => ColumnType::Text.def().null(),
Self::SpotifyState => ColumnType::Uuid.def(),
Self::RefCode => ColumnType::Text.def().null(),
Expand Down Expand Up @@ -302,3 +307,50 @@ impl Role {
matches!(self, Self::Admin)
}
}

#[derive(
Debug, Copy, Clone, EnumIter, DeriveActiveEnum, PartialEq, Eq, Default, Serialize, Deserialize,
)]
#[sea_orm(rs_type = "String", db_type = "Text")]
pub enum AISlopDetection {
#[sea_orm(string_value = "skip")]
Skip,
#[sea_orm(string_value = "notify")]
#[default]
Notify,
#[sea_orm(string_value = "ignore")]
Ignore,
}

impl FromStr for AISlopDetection {
type Err = sea_orm::DbErr;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s)
}
}

impl TryFrom<&str> for AISlopDetection {
type Error = sea_orm::DbErr;

fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::try_from_value(&value.to_owned())
}
}

impl AISlopDetection {
#[must_use]
pub fn is_skip(&self) -> bool {
matches!(self, Self::Skip)
}

#[must_use]
pub fn is_notify(&self) -> bool {
matches!(self, Self::Notify)
}

#[must_use]
pub fn is_ignore(&self) -> bool {
matches!(self, Self::Ignore)
}
}
1 change: 0 additions & 1 deletion src/lyrics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use genius::GeniusLocal;
use isolang::Language;
use lrclib::LrcLib;
use musixmatch::Musixmatch;
use serde::Serialize;
use strum_macros::Display;

use crate::spotify::ShortTrack;
Expand Down
Loading