added cargo files
This commit is contained in:
2712
PinePods-0.8.2/Backend/pinepods_backend/Cargo.lock
generated
Normal file
2712
PinePods-0.8.2/Backend/pinepods_backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
PinePods-0.8.2/Backend/pinepods_backend/Cargo.toml
Normal file
19
PinePods-0.8.2/Backend/pinepods_backend/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "pinepods_backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.11.0"
|
||||
serde = { version = "1.0.225", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
reqwest = { version = "0.12.23", features = ["json", "rustls-tls"] }
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.28"
|
||||
dotenvy = "0.15.7"
|
||||
sha1 = "0.10.6"
|
||||
urlencoding = "2.1.3"
|
||||
actix-cors = "0.7.1"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
642
PinePods-0.8.2/Backend/pinepods_backend/src/main.rs
Normal file
642
PinePods-0.8.2/Backend/pinepods_backend/src/main.rs
Normal file
@@ -0,0 +1,642 @@
|
||||
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
|
||||
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use dotenvy::dotenv;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use sha1::{Digest, Sha1};
|
||||
use log::{info, error};
|
||||
use actix_cors::Cors;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use chrono;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SearchQuery {
|
||||
query: Option<String>,
|
||||
index: Option<String>,
|
||||
search_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PodcastQuery {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YouTubeChannelQuery {
|
||||
id: String,
|
||||
}
|
||||
|
||||
// Hit counter for API usage tracking
|
||||
#[derive(Clone)]
|
||||
struct HitCounters {
|
||||
itunes_hits: Arc<AtomicU64>,
|
||||
podcast_index_hits: Arc<AtomicU64>,
|
||||
youtube_hits: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl HitCounters {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
itunes_hits: Arc::new(AtomicU64::new(0)),
|
||||
podcast_index_hits: Arc::new(AtomicU64::new(0)),
|
||||
youtube_hits: Arc::new(AtomicU64::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_itunes(&self) {
|
||||
self.itunes_hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn increment_podcast_index(&self) {
|
||||
self.podcast_index_hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn increment_youtube(&self) {
|
||||
self.youtube_hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn get_stats(&self) -> (u64, u64, u64) {
|
||||
(
|
||||
self.itunes_hits.load(Ordering::Relaxed),
|
||||
self.podcast_index_hits.load(Ordering::Relaxed),
|
||||
self.youtube_hits.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// YouTube API response structures for search
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct YouTubeSearchResponse {
|
||||
items: Vec<YouTubeChannelResult>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct YouTubeChannelResult {
|
||||
id: YouTubeChannelId,
|
||||
snippet: YouTubeChannelSnippet,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct YouTubeChannelId {
|
||||
#[serde(rename = "channelId")]
|
||||
channel_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct YouTubeChannelSnippet {
|
||||
title: String,
|
||||
description: String,
|
||||
thumbnails: YouTubeThumbnails,
|
||||
#[serde(rename = "channelTitle")]
|
||||
channel_title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct YouTubeThumbnails {
|
||||
default: Option<YouTubeThumbnail>,
|
||||
medium: Option<YouTubeThumbnail>,
|
||||
high: Option<YouTubeThumbnail>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct YouTubeThumbnail {
|
||||
url: String,
|
||||
}
|
||||
|
||||
// YouTube API response structures for channel details
|
||||
#[derive(Deserialize)]
|
||||
struct YouTubeChannelDetailsResponse {
|
||||
items: Vec<YouTubeChannelDetailsItem>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YouTubeChannelDetailsItem {
|
||||
snippet: YouTubeChannelDetailsSnippet,
|
||||
statistics: Option<YouTubeChannelStatistics>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YouTubeChannelDetailsSnippet {
|
||||
title: String,
|
||||
description: String,
|
||||
thumbnails: YouTubeThumbnails,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YouTubeChannelStatistics {
|
||||
#[serde(rename = "subscriberCount")]
|
||||
subscriber_count: Option<String>,
|
||||
#[serde(rename = "videoCount")]
|
||||
video_count: Option<String>,
|
||||
}
|
||||
|
||||
// YouTube API response structures for channel videos
|
||||
#[derive(Deserialize)]
|
||||
struct YouTubeVideosResponse {
|
||||
items: Vec<YouTubeVideoItem>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YouTubeVideoItem {
|
||||
id: YouTubeVideoId,
|
||||
snippet: YouTubeVideoSnippet,
|
||||
#[serde(rename = "contentDetails")]
|
||||
content_details: Option<YouTubeVideoContentDetails>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YouTubeVideoId {
|
||||
#[serde(rename = "videoId")]
|
||||
video_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YouTubeVideoSnippet {
|
||||
title: String,
|
||||
description: String,
|
||||
thumbnails: YouTubeThumbnails,
|
||||
#[serde(rename = "publishedAt")]
|
||||
published_at: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YouTubeVideoContentDetails {
|
||||
duration: Option<String>,
|
||||
}
|
||||
|
||||
// Simplified response format to match other APIs
|
||||
#[derive(Serialize)]
|
||||
struct YouTubeSearchResult {
|
||||
results: Vec<YouTubeChannel>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct YouTubeChannel {
|
||||
#[serde(rename = "channelId")]
|
||||
channel_id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
#[serde(rename = "thumbnailUrl")]
|
||||
thumbnail_url: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
// YouTube channel details response (when user clicks a channel)
|
||||
#[derive(Serialize)]
|
||||
struct YouTubeChannelDetails {
|
||||
#[serde(rename = "channelId")]
|
||||
channel_id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
#[serde(rename = "thumbnailUrl")]
|
||||
thumbnail_url: String,
|
||||
url: String,
|
||||
#[serde(rename = "subscriberCount")]
|
||||
subscriber_count: Option<i64>,
|
||||
#[serde(rename = "videoCount")]
|
||||
video_count: Option<i64>,
|
||||
#[serde(rename = "recentVideos")]
|
||||
recent_videos: Vec<YouTubeVideo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct YouTubeVideo {
|
||||
id: String,
|
||||
title: String,
|
||||
description: String,
|
||||
url: String,
|
||||
thumbnail: String,
|
||||
#[serde(rename = "publishedAt")]
|
||||
published_at: String,
|
||||
duration: Option<String>,
|
||||
}
|
||||
|
||||
async fn search_handler(
|
||||
query: web::Query<SearchQuery>,
|
||||
hit_counters: web::Data<HitCounters>,
|
||||
) -> impl Responder {
|
||||
println!("search_handler called");
|
||||
|
||||
if query.query.is_none() && query.index.is_none() {
|
||||
println!("Empty query and index - returning 200 OK");
|
||||
return HttpResponse::Ok().body("Test connection successful");
|
||||
}
|
||||
|
||||
let search_term = query.query.clone().unwrap_or_default();
|
||||
let index = query.index.clone().unwrap_or_default().to_lowercase();
|
||||
let search_type = query.search_type.clone().unwrap_or_else(|| "term".to_string());
|
||||
|
||||
println!("Received search request - Query: {}, Index: {}, Type: {}", search_term, index, search_type);
|
||||
println!("Searching for: {}", search_term);
|
||||
let client = reqwest::Client::new();
|
||||
println!("Client created");
|
||||
|
||||
let response = if index == "itunes" {
|
||||
// iTunes Search
|
||||
hit_counters.increment_itunes();
|
||||
let itunes_search_url = format!("https://itunes.apple.com/search?term={}&media=podcast", search_term);
|
||||
println!("Using iTunes search URL: {}", itunes_search_url);
|
||||
|
||||
client.get(&itunes_search_url).send().await
|
||||
} else if index == "youtube" {
|
||||
// YouTube Data API v3 Search
|
||||
hit_counters.increment_youtube();
|
||||
return search_youtube_channels(&search_term).await;
|
||||
} else {
|
||||
// Podcast Index API search
|
||||
hit_counters.increment_podcast_index();
|
||||
let (api_key, api_secret) = match get_api_credentials() {
|
||||
Ok(creds) => creds,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let encoded_search_term = urlencoding::encode(&search_term);
|
||||
println!("Encoded search term: {}", encoded_search_term);
|
||||
println!("Search type: {}", search_type);
|
||||
let podcast_search_url = match search_type.as_str() {
|
||||
"person" => {
|
||||
println!("Using /search/byperson endpoint");
|
||||
format!("https://api.podcastindex.org/api/1.0/search/byperson?q={}", encoded_search_term)
|
||||
},
|
||||
_ => {
|
||||
println!("Using /search/byterm endpoint");
|
||||
format!("https://api.podcastindex.org/api/1.0/search/byterm?q={}", encoded_search_term)
|
||||
},
|
||||
};
|
||||
|
||||
println!("Using Podcast Index search URL: {}", podcast_search_url);
|
||||
|
||||
let headers = match create_auth_headers(&api_key, &api_secret) {
|
||||
Ok(h) => h,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
println!("Final Podcast Index URL: {}", podcast_search_url);
|
||||
|
||||
client.get(&podcast_search_url).headers(headers).send().await
|
||||
};
|
||||
|
||||
handle_response(response).await
|
||||
}
|
||||
|
||||
async fn search_youtube_channels(search_term: &str) -> HttpResponse {
|
||||
println!("Searching YouTube for: {}", search_term);
|
||||
|
||||
let youtube_api_key = match env::var("YOUTUBE_API_KEY") {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
error!("YOUTUBE_API_KEY not set in the environment");
|
||||
return HttpResponse::InternalServerError().body("YouTube API key not configured");
|
||||
}
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let encoded_search_term = urlencoding::encode(search_term);
|
||||
|
||||
// YouTube Data API v3 search for channels
|
||||
let youtube_search_url = format!(
|
||||
"https://www.googleapis.com/youtube/v3/search?part=snippet&type=channel&q={}&maxResults=25&key={}",
|
||||
encoded_search_term, youtube_api_key
|
||||
);
|
||||
|
||||
println!("Using YouTube search URL: {}", youtube_search_url);
|
||||
|
||||
match client.get(&youtube_search_url).send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
match resp.json::<YouTubeSearchResponse>().await {
|
||||
Ok(youtube_response) => {
|
||||
// Convert YouTube response to our format
|
||||
let channels: Vec<YouTubeChannel> = youtube_response.items.into_iter().map(|item| {
|
||||
let thumbnail_url = item.snippet.thumbnails.high
|
||||
.or(item.snippet.thumbnails.medium)
|
||||
.or(item.snippet.thumbnails.default)
|
||||
.map(|thumb| thumb.url)
|
||||
.unwrap_or_default();
|
||||
|
||||
YouTubeChannel {
|
||||
channel_id: item.id.channel_id.clone(),
|
||||
name: item.snippet.title,
|
||||
description: item.snippet.description,
|
||||
thumbnail_url,
|
||||
url: format!("https://www.youtube.com/channel/{}", item.id.channel_id),
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let result = YouTubeSearchResult { results: channels };
|
||||
|
||||
match serde_json::to_string(&result) {
|
||||
Ok(json_response) => {
|
||||
println!("YouTube search successful, found {} channels", result.results.len());
|
||||
HttpResponse::Ok().content_type("application/json").body(json_response)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to serialize YouTube response: {}", e);
|
||||
HttpResponse::InternalServerError().body("Failed to process YouTube response")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to parse YouTube API response: {}", e);
|
||||
HttpResponse::InternalServerError().body("Failed to parse YouTube response")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("YouTube API request failed with status: {}", resp.status());
|
||||
HttpResponse::InternalServerError().body(format!("YouTube API error: {}", resp.status()))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("YouTube API request error: {}", e);
|
||||
HttpResponse::InternalServerError().body("YouTube API request failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn podcast_handler(
|
||||
query: web::Query<PodcastQuery>,
|
||||
hit_counters: web::Data<HitCounters>,
|
||||
) -> impl Responder {
|
||||
println!("podcast_handler called");
|
||||
hit_counters.increment_podcast_index();
|
||||
|
||||
let podcast_id = &query.id;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let (api_key, api_secret) = match get_api_credentials() {
|
||||
Ok(creds) => creds,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let podcast_url = format!("https://api.podcastindex.org/api/1.0/podcasts/byfeedid?id={}", podcast_id);
|
||||
println!("Using Podcast Index URL: {}", podcast_url);
|
||||
|
||||
let headers = match create_auth_headers(&api_key, &api_secret) {
|
||||
Ok(h) => h,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let response = client.get(&podcast_url).headers(headers).send().await;
|
||||
handle_response(response).await
|
||||
}
|
||||
|
||||
async fn youtube_channel_handler(
|
||||
query: web::Query<YouTubeChannelQuery>,
|
||||
hit_counters: web::Data<HitCounters>,
|
||||
) -> impl Responder {
|
||||
println!("youtube_channel_handler called for channel: {}", query.id);
|
||||
hit_counters.increment_youtube();
|
||||
|
||||
let youtube_api_key = match env::var("YOUTUBE_API_KEY") {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
error!("YOUTUBE_API_KEY not set in the environment");
|
||||
return HttpResponse::InternalServerError().body("YouTube API key not configured");
|
||||
}
|
||||
};
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let channel_id = &query.id;
|
||||
|
||||
// Step 1: Get channel details and statistics
|
||||
let channel_details_url = format!(
|
||||
"https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&id={}&key={}",
|
||||
channel_id, youtube_api_key
|
||||
);
|
||||
|
||||
println!("Fetching channel details: {}", channel_details_url);
|
||||
|
||||
let channel_details = match client.get(&channel_details_url).send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
match resp.json::<YouTubeChannelDetailsResponse>().await {
|
||||
Ok(details) => {
|
||||
if details.items.is_empty() {
|
||||
return HttpResponse::NotFound().body("Channel not found");
|
||||
}
|
||||
details.items.into_iter().next().unwrap()
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to parse channel details: {}", e);
|
||||
return HttpResponse::InternalServerError().body("Failed to parse channel details");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Channel details request failed with status: {}", resp.status());
|
||||
return HttpResponse::InternalServerError().body(format!("YouTube API error: {}", resp.status()));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Channel details request error: {}", e);
|
||||
return HttpResponse::InternalServerError().body("YouTube API request failed");
|
||||
}
|
||||
};
|
||||
|
||||
// Step 2: Get recent videos from the channel
|
||||
let videos_url = format!(
|
||||
"https://www.googleapis.com/youtube/v3/search?part=snippet&channelId={}&type=video&order=date&maxResults=10&key={}",
|
||||
channel_id, youtube_api_key
|
||||
);
|
||||
|
||||
println!("Fetching recent videos: {}", videos_url);
|
||||
|
||||
let videos = match client.get(&videos_url).send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
match resp.json::<YouTubeVideosResponse>().await {
|
||||
Ok(videos_response) => {
|
||||
videos_response.items.into_iter().map(|item| {
|
||||
let thumbnail_url = item.snippet.thumbnails.medium
|
||||
.or(item.snippet.thumbnails.high)
|
||||
.or(item.snippet.thumbnails.default)
|
||||
.map(|thumb| thumb.url)
|
||||
.unwrap_or_default();
|
||||
|
||||
YouTubeVideo {
|
||||
id: item.id.video_id.clone(),
|
||||
title: item.snippet.title,
|
||||
description: item.snippet.description,
|
||||
url: format!("https://www.youtube.com/watch?v={}", item.id.video_id),
|
||||
thumbnail: thumbnail_url,
|
||||
published_at: item.snippet.published_at,
|
||||
duration: item.content_details.and_then(|cd| cd.duration),
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to parse videos response: {}", e);
|
||||
return HttpResponse::InternalServerError().body("Failed to parse videos");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Videos request failed with status: {}", resp.status());
|
||||
// Continue without videos rather than failing completely
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Videos request error: {}", e);
|
||||
// Continue without videos rather than failing completely
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
// Extract thumbnail URL from channel details
|
||||
let thumbnail_url = channel_details.snippet.thumbnails.high
|
||||
.or(channel_details.snippet.thumbnails.medium)
|
||||
.or(channel_details.snippet.thumbnails.default)
|
||||
.map(|thumb| thumb.url)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Parse subscriber and video counts
|
||||
let subscriber_count = channel_details.statistics.as_ref()
|
||||
.and_then(|stats| stats.subscriber_count.as_ref())
|
||||
.and_then(|count| count.parse::<i64>().ok());
|
||||
|
||||
let video_count = channel_details.statistics.as_ref()
|
||||
.and_then(|stats| stats.video_count.as_ref())
|
||||
.and_then(|count| count.parse::<i64>().ok());
|
||||
|
||||
let result = YouTubeChannelDetails {
|
||||
channel_id: channel_id.to_string(),
|
||||
name: channel_details.snippet.title,
|
||||
description: channel_details.snippet.description,
|
||||
thumbnail_url,
|
||||
url: format!("https://www.youtube.com/channel/{}", channel_id),
|
||||
subscriber_count,
|
||||
video_count,
|
||||
recent_videos: videos,
|
||||
};
|
||||
|
||||
match serde_json::to_string(&result) {
|
||||
Ok(json_response) => {
|
||||
println!("YouTube channel details successful for {}, found {} videos", result.name, result.recent_videos.len());
|
||||
HttpResponse::Ok().content_type("application/json").body(json_response)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to serialize channel details response: {}", e);
|
||||
HttpResponse::InternalServerError().body("Failed to process channel details")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn stats_handler(hit_counters: web::Data<HitCounters>) -> impl Responder {
|
||||
let (itunes, podcast_index, youtube) = hit_counters.get_stats();
|
||||
|
||||
let stats = serde_json::json!({
|
||||
"api_usage": {
|
||||
"itunes_hits": itunes,
|
||||
"podcast_index_hits": podcast_index,
|
||||
"youtube_hits": youtube,
|
||||
"total_hits": itunes + podcast_index + youtube
|
||||
},
|
||||
"timestamp": chrono::Utc::now().to_rfc3339()
|
||||
});
|
||||
|
||||
HttpResponse::Ok().content_type("application/json").json(stats)
|
||||
}
|
||||
|
||||
fn get_api_credentials() -> Result<(String, String), HttpResponse> {
|
||||
let api_key = match env::var("API_KEY") {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
println!("API_KEY not set in the environment");
|
||||
return Err(HttpResponse::InternalServerError().body("API_KEY not set"));
|
||||
}
|
||||
};
|
||||
let api_secret = match env::var("API_SECRET") {
|
||||
Ok(secret) => secret,
|
||||
Err(_) => {
|
||||
println!("API_SECRET not set in the environment");
|
||||
return Err(HttpResponse::InternalServerError().body("API_SECRET not set"));
|
||||
}
|
||||
};
|
||||
Ok((api_key, api_secret))
|
||||
}
|
||||
|
||||
fn create_auth_headers(api_key: &str, api_secret: &str) -> Result<HeaderMap, HttpResponse> {
|
||||
let epoch_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs().to_string();
|
||||
let data_to_hash = format!("{}{}{}", api_key, api_secret, epoch_time);
|
||||
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(data_to_hash.as_bytes());
|
||||
let sha_1 = format!("{:x}", hasher.finalize());
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("X-Auth-Date", HeaderValue::from_str(&epoch_time).unwrap_or_else(|e| {
|
||||
error!("Failed to insert X-Auth-Date header: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}));
|
||||
headers.insert("X-Auth-Key", HeaderValue::from_str(api_key).unwrap_or_else(|e| {
|
||||
error!("Failed to insert X-Auth-Key header: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}));
|
||||
headers.insert("Authorization", HeaderValue::from_str(&sha_1).unwrap_or_else(|e| {
|
||||
error!("Failed to insert Authorization header: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}));
|
||||
headers.insert(USER_AGENT, HeaderValue::from_static("PodPeopleDB/1.0"));
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
async fn handle_response(response: Result<reqwest::Response, reqwest::Error>) -> HttpResponse {
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
println!("Request succeeded");
|
||||
match resp.text().await {
|
||||
Ok(body) => {
|
||||
println!("Response body: {:?}", body);
|
||||
HttpResponse::Ok().content_type("application/json").body(body)
|
||||
},
|
||||
Err(_) => {
|
||||
error!("Failed to parse response body");
|
||||
HttpResponse::InternalServerError().body("Failed to parse response body")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Request failed with status code: {}", resp.status());
|
||||
println!("Request Headers: {:?}", resp.headers());
|
||||
HttpResponse::InternalServerError().body(format!("Request failed with status code: {}", resp.status()))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Request error: {:?}", err);
|
||||
HttpResponse::InternalServerError().body("Request error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv().ok();
|
||||
env_logger::init();
|
||||
|
||||
println!("Starting the Actix Web server with yt search");
|
||||
|
||||
// Initialize hit counters
|
||||
let hit_counters = web::Data::new(HitCounters::new());
|
||||
|
||||
HttpServer::new(move || {
|
||||
let cors = Cors::default()
|
||||
.allow_any_origin() // Allow all origins since this is self-hostable
|
||||
.allow_any_method() // Allow all HTTP methods
|
||||
.allow_any_header() // Allow all headers
|
||||
.supports_credentials()
|
||||
.max_age(3600); // Cache preflight requests for 1 hour
|
||||
|
||||
App::new()
|
||||
.app_data(hit_counters.clone())
|
||||
.wrap(cors)
|
||||
.route("/api/search", web::get().to(search_handler))
|
||||
.route("/api/podcast", web::get().to(podcast_handler))
|
||||
.route("/api/youtube/channel", web::get().to(youtube_channel_handler))
|
||||
.route("/api/stats", web::get().to(stats_handler))
|
||||
})
|
||||
.bind("0.0.0.0:5000")?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
Reference in New Issue
Block a user