Table Of Contents
English
Introduction
I had to make a Browser Extension that notifies the user when a new video is posted and when the Twitch streamer launch a stream.
For this extension I needed to set up an API to centralize my requests to the APIs of Google and Twitch.
I decided to innovate a bit, and to make this API in Rust rather than with NodeJS or PHP. With that, I decided to go further and separate my Twitch and Youtube requests in two different CLI applications.
My main application communicates with my two sub-applications with std::process::Command
.
Twitch && Youtube CLI application
My goal was to make my queries to the Twitch API and the Youtube API with its two applications, which, once the queries were made, returned the results.
Results that I retrieve in my main application.
Here is an example to retrieve the last posted Youtube video:
async fn get_last_video(&self, channel_id: String) -> Result<serde_json::Value, reqwest::Error> {
let base_url: String = "https://www.googleapis.com/youtube/v3/search".to_owned();
let client = reqwest::Client::new();
let resp = client.get(format!("{}?part=snippet&channelId={}&maxResults=1&order=date&type=video&key={}", &base_url, &channel_id, &self.youtube_api_key).as_str()).send().await?;
let text = resp.text().await?;
let video: serde_json::Value = serde_json::from_str(&text).unwrap();
Ok(video)
}
In this example, I use serde_json and reqwest.
Main application
To set up my API I used actix-web where I could define my routes like this:
use crate::api_error::ApiError;
use actix_web::{get, web, HttpResponse};
use std::fs;
#[get("/")]
async fn get_home() -> Result<HttpResponse, ApiError> {
let error = ApiError::new(404, "Error".to_owned());
Ok(HttpResponse::Ok().json(error))
}
#[get("/twitch/stream")]
async fn get_twitch() -> Result<HttpResponse, ApiError> {
let file_data = read_file("paranoi4k_twitch.json".to_owned()).await;
let serialized: serde_json::Value = serde_json::from_str(&file_data.as_str()).unwrap();
Ok(HttpResponse::Ok().json(serialized))
}
pub fn init_routes(cfg: &mut web::ServiceConfig) {
cfg.service(get_home);
cfg.service(get_twitch);
}
We can see that for the /twitch/stream
route, I retrieve data in a paranoi4k_twitch.json
file at the root of my application.
Indeed, when I use my Twitch CLI, I save the data it returns to me in a json file, so that I can retrieve it in my route whenever I want.
This is how I save my json file:
pub fn check_stream(&self) -> std::io::Result<()> {
let path = self.make_path();
let app_command = format!("isonlive-user={}", &self.uid);
let output = Command::new(path)
.arg(app_command)
.output()
.unwrap();
let encoded = String::from_utf8_lossy(output.stdout.as_slice());
let serialized: serde_json::Value = serde_json::from_str(&encoded).unwrap();
let mut buffer = File::create(&self.filename).unwrap();
buffer.write_fmt(format_args!("{}", serialized)).unwrap();
Ok(())
}
When I retrieve data from my CLI application, it is displayed as bytes, so I have to use the from_utf8_lossy
method to convert to a string.
And Voilà, that's basically all I set up for this Api, it was more of a personal challenge in the end.
Extension
For my web extension written in JavaScript, I have a function like this to call my API :
/**
* @param {string} socialname
* @return {output} { status: false, data: false }
* @exemple let request = await rq("twitch");
*/
async rq(socialname) {
let output = { status: false, data: false };
let url = socialname === "twitch" ? this.twitch_url : this.youtube_url;
try {
let response = await fetch(url, {
headers: {
"Content-Type": "application/json"
},
method: "GET"
});
if (response.ok) {
let responseTarget = await response.json();
output.status = true;
if (socialname === "youtube") {
output.data = responseTarget.items[0];
} else if (socialname === "twitch") {
output.data = responseTarget.data;
}
} else {
output.status = false;
output.data = "Problème de serveur, vous pouvez me contacter ici twitter.com/zaekof";
}
} catch (error) {
output.status = false;
output.data = error;
}
return output;
}
I think I've explained the main thing, don't hesitate to correct me and make constructive criticism :)
Useful links:
RustYoutubeCli
RustTwitchCli
InfluencerApi
French
Introduction
J'ai eu à réaliser une Extension pour navigateur qui avertit l'utilisateur quand une nouvelle vidéo est postée et quand le streamer Twitch lance un live.
Pour cette extension j'ai eu besoin de mettre en place une API pour centraliser mes requêtes vers les API de Google et de Twitch.
J'ai alors décidé d’innover un peu, et de faire cette API en Rust plutôt qu'avec du NodeJS ou du PHP. Et en plus de ça, j'ai décidé d'aller plus loin et de séparer mes requêtes Twitch et mes requêtes Youtube dans deux sou applications CLI différentes.
Mon application principal communique avec elles avec std::process::Command
.
Twitch && Youtube CLI application
Mon but était de faire mes requêtes vers l'API de Twitch et l'API de Youtube via ses deux applications, qui une fois les requêtes effectuées me retourne les résultats.
Résultats que je récupère dans mon application principale.
Voici un exemple pour récupérer la dernière vidéo Youtube posté :
async fn get_last_video(&self, channel_id: String) -> Result<serde_json::Value, reqwest::Error> {
let base_url: String = "https://www.googleapis.com/youtube/v3/search".to_owned();
let client = reqwest::Client::new();
let resp = client.get(format!("{}?part=snippet&channelId={}&maxResults=1&order=date&type=video&key={}", &base_url, &channel_id, &self.youtube_api_key).as_str()).send().await?;
let text = resp.text().await?;
let video: serde_json::Value = serde_json::from_str(&text).unwrap();
Ok(video)
}
Dans cette exemple, j'utilise serde_json et reqwest.
Application principal
Pour la mise en place de mon API j'ai donc utilisé actix-web ou j'ai pu définir mes routes comme ceci :
use crate::api_error::ApiError;
use actix_web::{get, web, HttpResponse};
use std::fs;
#[get("/")]
async fn get_home() -> Result<HttpResponse, ApiError> {
let error = ApiError::new(404, "Error".to_owned());
Ok(HttpResponse::Ok().json(error))
}
#[get("/twitch/stream")]
async fn get_twitch() -> Result<HttpResponse, ApiError> {
let file_data = read_file("paranoi4k_twitch.json".to_owned()).await;
let serialized: serde_json::Value = serde_json::from_str(&file_data.as_str()).unwrap();
Ok(HttpResponse::Ok().json(serialized))
}
pub fn init_routes(cfg: &mut web::ServiceConfig) {
cfg.service(get_home);
cfg.service(get_twitch);
}
On peut voir que pour la route /twitch/stream
, je récupère des données dans un fichier paranoi4k_twitch.json
à la racine de mon application.
En effet, quand je fais appel à mon Twitch CLI, j'enregistre les données qu'il me retourne dans un fichier json, de manière à pouvoir les récupérer dans ma route quand je veux.
Voici comment j'enregistre mon fichier json :
pub fn check_stream(&self) -> std::io::Result<()> {
let path = self.make_path();
let app_command = format!("isonlive-user={}", &self.uid);
let output = Command::new(path)
.arg(app_command)
.output()
.unwrap();
let encoded = String::from_utf8_lossy(output.stdout.as_slice());
let serialized: serde_json::Value = serde_json::from_str(&encoded).unwrap();
let mut buffer = File::create(&self.filename).unwrap();
buffer.write_fmt(format_args!("{}", serialized)).unwrap();
Ok(())
}
Quand je récupère les données de mon application CLI, celles-ci sont affichées sous forme de bytes, je dois donc utiliser la méthode from_utf8_lossy
pour convertir en String.
Voilà en gros un peu tout ce que j'ai mis en place pour cette Api, c'était plus un défi personnel au final.
Extension
Dans le cadre de mon extension web écrite en JavaScript, j'ai une fonction comme ceci pour faire appel à mon API :
/**
* @param {string} socialname
* @return {output} { status: false, data: false }
* @exemple let request = await rq("twitch");
*/
async rq(socialname) {
let output = { status: false, data: false };
let url = socialname === "twitch" ? this.twitch_url : this.youtube_url;
try {
let response = await fetch(url, {
headers: {
"Content-Type": "application/json"
},
method: "GET"
});
if (response.ok) {
let responseTarget = await response.json();
output.status = true;
if (socialname === "youtube") {
output.data = responseTarget.items[0];
} else if (socialname === "twitch") {
output.data = responseTarget.data;
}
} else {
output.status = false;
output.data = "Problème de serveur, vous pouvez me contacter ici twitter.com/zaekof";
}
} catch (error) {
output.status = false;
output.data = error;
}
return output;
}
Je pense vous avoir expliqué le principal, n'hésitez pas à me corriger et à faire des critiques constructives :)
Liens utiles :
RustYoutubeCli
RustTwitchCli
InfluencerApi
Top comments (2)
Very cool! If you have not already, you should check out StructOpt for parsing your command line arguments.
Oh, Thanks. I really appreciate.