bitdispenser.dev/org/posts/songlify-1.org
Davide Polonio 06e0e6afe8 refactor: migrate from Hugo to Org-mode static site generator
- Remove Hugo configuration, themes, and Node.js dependencies
- Delete CI/CD files (.drone.yml, .fleek.json, Dockerfile)
- Convert Markdown content to Org-mode format in org/ directory
- Add Emacs Lisp publishing script with ox-publish configuration
- Create Makefile for build automation
- Update .gitignore for Emacs and macOS files
- Preserve media assets with Git LFS tracking
- Add RSS feed generation capability
- Remove package.json, package-lock.json, and markdownlint config
2025-09-23 17:07:02 +02:00

273 lines
11 KiB
Org Mode

Some time passed since the last article I wrote there. A lot of stuff
happened meanwhile, especially with COVID, but here we are again. While
busy dealing with the mess of real life tasks, three months ago I
started to write a little bot for Telegram in Rust. It is a simple one,
but I consider the journey interesting and worth of writing it down. If
I add new features worth mentioning I will start a series about it,
maybe.
** Telegram bots
:PROPERTIES:
:CUSTOM_ID: telegram-bots
:END:
Telegram bots are not something new to me and nowadays are pretty much
easy to make, so I consider them like a gym where to try out new
technologies and experiment with stuff. I wrote plentiful of them, some
of those are open source like for example
[[https://github.com/Augugrumi/TorreArchimedeBot]] (which is currently
broken 😭) that was useful when going to University, because it scraped
the university free room web page and from there it was able to tell you
which rooms where without any lessons and for how much time, allowing
you to easily find a place where to study with your mates (yep, we
didn't like library too much).
#+CAPTION: A screenshot of TorreArchimedeBot in action.
#+NAME: fig:A screenshot of TorreArchimedeBot in action.
#+ATTR_HTML: :width 600px
[[../media/songlify/telegramscreen.png]]
Also another one bot worthy of mention is
[[https://github.com/Polpetta/RedditToTelegram]], that allowed our D&D
group to receive push notifications of our private Subreddit in our
Telegram group.
As you can see, all of these bots are quite simple, but they have the
added value of teaching you some new programming concepts, technologies
or frameworks that can be later applied in something that can be more
production environment.
** Rust
:PROPERTIES:
:CUSTOM_ID: rust
:END:
I started to approach Rust many years ago (I do not remember exactly
when). First interaction with it was quite interesting to say at least:
there were way less compiler features (for example now the compiler is
able to understand object lifetime at compile time most of the time
alone, without specifying them) that made it a... /not-so-pleasant
programming experience/. It had potential thought, so by following Rust
news I picked it up last year again, noticing that now it has improved a
lot and it is more pleasant to write. Meanwhile, also JetBrains
developed a good support for IntelliJ, so now it is even possible to
debug and perform every operation directly from your IDE UI.
** Making the two worlds collide: Rust + Telegram
:PROPERTIES:
:CUSTOM_ID: making-the-two-worlds-collide-rust-telegram
:END:
One of the features I wanted to learn this time regarding Rust was the
asynchronous support it offers. Rust started to have =async= support
with [[https://tokio.rs/][Tokio]] framework, and recently the Rust team
started to build the asynchronous functionality inside Rust itself. Even
if in the first steps, it looks promising and the idea of a low-level
language, without GC, with automatic memory management and so much
safety having asynchronous support is exciting to me! 🥳 So the only
option left, at this point, was to start messing around with it. I
started by picking up one of the many frameworks that provides a layer
for the Telegram APIs,
[[https://github.com/teloxide/teloxide][Teloxide]]. In particular, as
you can see from its /README/, one of the examples starts by using
=#[tokio:main]= macro:
#+begin_src rust
use teloxide::prelude::*;
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting dices_bot...");
let bot = Bot::from_env().auto_send();
teloxide::repl(bot, |message| async move {
message.answer_dice().await?;
respond(())
})
.await;
}
#+end_src
This was the reason I picked it up, given that it looked the most
promising by the time I started the project.
*** Building /Songlify/
:PROPERTIES:
:CUSTOM_ID: building-songlify
:END:
So, after choosing what was going to use to build the bot, I needed a
/reason/ to build it.
With my friends we usually share a lot of songs (via Spotify links), so
I thought it was a good idea to build a bot around it. I integrated a
Spotify API library in it and started hacking up a bot.
#+begin_quote
⚠ Note that at the time of writing I have just noticed that the library
I use for speaking with Spotify,
[[https://crates.io/crates/aspotify][aspotify]] has been deprecated in
favour of [[https://crates.io/crates/rspotify][rspotify]]
#+end_quote
The first bot version was something very simple, and it was a
single-file program with nothing very fancy (I have written it in a
night):
#+begin_src rust
use crate::SpotifyURL::Track;
use aspotify::{Client, ClientCredentials};
use teloxide::prelude::*;
enum SpotifyURL {
Track(String),
}
fn get_spotify_entry(url: &str) -> Option<SpotifyURL> {
if url.contains("https://open.spotify.com/track/") {
let track_id = url.rsplit('/').next().and_then(|x| x.split('?').next());
return match track_id {
Some(id) => Some(SpotifyURL::Track(id.to_string())),
None => None,
};
}
return None;
}
struct TrackInfo {
name: String,
artist: Vec<String>,
}
async fn get_spotify_track(spotify: Box<Client>, id: &String) -> Option<TrackInfo> {
match spotify.tracks().get_track(id.as_str(), None).await {
Ok(track) => Some(TrackInfo {
name: track.data.name,
artist: track.data.artists.iter().map(|x| x.name.clone()).collect(),
}),
Err(_e) => None,
}
}
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting Songlify...");
let bot = Bot::from_env().auto_send();
teloxide::repl(bot, |message| async move {
let spotify_creds =
ClientCredentials::from_env().expect("CLIENT_ID and CLIENT_SECRET not found.");
let spotify_client = Box::new(Client::new(spotify_creds));
log::info!("Connected to Spotify");
let text = message.update.text().and_then(get_spotify_entry);
match text {
Some(spotify) => match spotify {
Track(id) => {
let track_info = get_spotify_track(spotify_client, &id).await;
match track_info {
Some(info) => {
let reply = format!(
"Track information:\n\
Track name: {}\n\
Artists: {}",
info.name,
info.artist.join(", ")
);
Some(message.reply_to(reply).await?)
}
None => None,
}
}
},
None => None,
};
respond(())
})
.await;
log::info!("Exiting...");
}
#+end_src
As you can see, basically every time a request arrived to the bot, login
to Spotify was performed and track information and name retrieved from
there. Of course this was only the beginning (also you can “appreciate”
the number of nested blocks there...). Now the bot supports albums and
playlists too, with the possibility to go through each song in the
playlist and collect general information such as how many artists are in
that playlist, how many songs and other little information like that. If
you see the
[[https://git.poldebra.me/polpetta/Songlify][bot repository]] you can
see now that Spotify functions live in a separate module.
**** Packaging and distribution
:PROPERTIES:
:CUSTOM_ID: packaging-and-distribution
:END:
The obvious choice for a software like that was to incorporate it into a
OCI image. I wrote a very simple Dockerfile that, once the program was
built, took the artifact and using the
[[https://docs.docker.com/develop/develop-images/multistage-build/][multi-stage Docker build functionality]]
and put it into a separate container, in order to avoid having build
dependencies inside the final image. I used the images distributed by
the [[file:gcr.io/distroless/][Distroless project]] (you can find the
source on their
[[https://github.com/GoogleContainerTools/distroless][Github repository]])
in order to obtain the smallest possible image. The final result?
#+begin_src txt
λ ~/Desktop/git/songlify/ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test/test latest 8ac7a7018719 5 seconds ago 34MB
<none> <none> 4bc7fb0699e0 12 seconds ago 1.53GB
rust 1.56.1-slim-bullseye d3e070c5ffa7 6 weeks ago 667MB
gcr.io/distroless/base latest-amd64 24787c1cd2e4 52 years ago 20.2MB
#+end_src
A part from the 52 years old image pulled from =gcr=, you can see that
=test/test= (actually Songlify) is only of *34MB*. Not much if you
consider that inside that image there are shared dynamic libraries to
make the executable able to run, which by default weights 20MB. A plus
of these images is that they do not run as root user and they do not
have any shell of bash integrated, making a possible surface attack
smaller (not that Docker is secure anyway...). I upload the images on
Docker Hub, where you can find them here
[[https://hub.docker.com/r/polpetta/songlify]]
Finally, to run the bot I use a very simple docker-compose definition,
that can be found in my
[[https://git.poldebra.me/polpetta/server-dotfiles/src/commit/7e7e1780b2db45f475510c49bf1a2f9e76c4c166/songlify/docker-compose.yml][server-dotfiles repository]].
This allows me to easily upgrade the bot by just changing the version
and running =docker-compose up -d=.
**** Plans for the future
:PROPERTIES:
:CUSTOM_ID: plans-for-the-future
:END:
Currently I work on the bot only when I feel like I wanna add something.
An interesting feature to add could be to insert a persistence layer
(using a database for instance) and add various stats (which is the most
shared song? Who /is that guy/ that shares the most songs in a group?).
Persistence can be achieved quite easily by using
[[https://diesel.rs/][Diesel]], an ORM compatible with various
databases.
Another cool feature could be to add the /inline bot/ functionality,
where you can search for songs directly in Spotify. Since I currently
have a domain available for that I could set it up for receive web-hook
notifications, instead of performing polling like the bot is currently
doing (one requisite for inline bots is indeed to receive web-hooks).
There are platforms like Heroku where you could make the bot run, but
currently I prefer to use my box since it gives me more flexibility.
Experimenting with Heroku could lead to cool results though 😏.
Finally, link translation could be something very useful. I have a
friend that does not use Spotify but prefers to listen to music via
YouTube. So an interesting feature would be, given a Spotify link, to
“convert” it into a YouTube link and, of course, /vice-versa/. This
could lead to translate playlists and albums too into YouTube-based
playlists, which of course could be very useful if you are trying to
avoid the infamous /vendor lock-in/, in this case being stuck with
Spotify because all you music collection, saved songs, etc is there.