Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 24 additions & 12 deletions backend/src/fs_util.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
// Libs
use std::path::PathBuf;

use anyhow::Context;
use bytes::Bytes;
use dirs::home_dir;
use futures_util::{Stream, StreamExt};
use path_absolutize::Absolutize;
use rust_embed::RustEmbed;
use tokio::{fs, io::AsyncWriteExt};

use crate::{
app_util::is_container,
config_util::{get_config, is_debug},
rt_util::QuitUnwrap,
};
use csv::ReaderBuilder;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct CsvRecord {
field1: String,
field2: String,
// Add more fields as needed
}

// Structs
/// Holds all the static files for UFC Ripper GUI that will be served using axum.
#[derive(RustEmbed, Clone)]
#[folder = "$CARGO_MANIFEST_DIR/../dist/"]
pub struct WebAssets;

/// Reads the config.json file from the disk and returns the content as `String`.
/// Will create the default config file if it doesn't exist.
pub async fn read_config_file_to_string(path: &PathBuf) -> String {
let read = async {
fs::read_to_string(path).await.unwrap_or_quit(
Expand Down Expand Up @@ -53,7 +55,6 @@ pub async fn read_config_file_to_string(path: &PathBuf) -> String {
}
}

/// Writes the current configuration to config.json file.
pub async fn write_config_to_file(path: &PathBuf) -> anyhow::Result<()> {
let mut conf_file = fs::File::create(path).await?;

Expand All @@ -64,7 +65,6 @@ pub async fn write_config_to_file(path: &PathBuf) -> anyhow::Result<()> {
Ok(())
}

/// Creates a file on the disk using the given byte-stream.
pub async fn write_file_to_disk<S>(
path: PathBuf,
size: u64,
Expand Down Expand Up @@ -104,16 +104,13 @@ where
Ok(())
}

/// Opens the downloads directory in the default file explorer.
pub fn open_downloads_dir() -> anyhow::Result<()> {
open::that_detached(&get_config().dl_path)
.context("An error occurred while trying to open the downloads directory")?;

Ok(())
}

/// Generates the path to downloads directory depending on the source path and the OS
/// and returns it as a String.
pub fn build_downloads_dir_path(org_dl_path: String) -> anyhow::Result<String> {
if is_container() {
Ok("/downloads".to_string())
Expand All @@ -134,3 +131,18 @@ pub fn build_downloads_dir_path(org_dl_path: String) -> anyhow::Result<String> {
Ok(org_dl_path)
}
}

pub async fn parse_csv_file(file_path: PathBuf) -> anyhow::Result<Vec<CsvRecord>> {
let mut rdr = ReaderBuilder::new()
.has_headers(true)
.from_path(file_path)
.context("Failed to open CSV file")?;

let mut records = Vec::new();
for result in rdr.deserialize() {
let record: CsvRecord = result.context("Failed to parse CSV record")?;
records.push(record);
}

Ok(records)
}
79 changes: 55 additions & 24 deletions backend/src/net_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,25 @@ use axum::{
body::Body,
http::{header, Method, StatusCode},
response::IntoResponse,
routing::get,
routing::{get, post},
Router,
Json,
};
use axum_embed::{FallbackBehavior::Redirect, ServeEmbed};
use once_cell::sync::Lazy;
use reqwest::{header::HeaderMap, Client, Proxy, Response};
use serde_json::{json, value::Index, Value};
use tokio::net::TcpListener;
use tower_http::cors::{Any, CorsLayer};
use axum::extract::Multipart;

use ufcr_libs::{log_err, log_success};

use crate::{
app_util::{get_app_metadata, get_os_id, is_container},
bin_util::BINS,
config_util::{get_config, is_debug, update_config, ConfigUpdate, UFCRConfig},
fs_util::{write_file_to_disk, WebAssets},
fs_util::{write_file_to_disk, WebAssets, parse_csv_file},
rt_util::QuitUnwrap,
state_util::Vod,
txt_util::get_vod_id_from_url,
Expand Down Expand Up @@ -100,6 +102,7 @@ pub async fn init_server() {
let app = Router::new()
.nest_service("/", web_assets)
.route("/export_config", get(handle_config_dl_req))
.route("/upload_csv", post(handle_csv_upload))
.layer(create_ws_layer())
.layer(create_cors_layer());

Expand Down Expand Up @@ -129,7 +132,7 @@ pub async fn init_server() {
/// Creates a new Tower layer with CORS rules.
fn create_cors_layer() -> CorsLayer {
CorsLayer::new()
.allow_methods([Method::GET])
.allow_methods([Method::GET, Method::POST])
.allow_origin(Any)
}

Expand Down Expand Up @@ -187,6 +190,34 @@ async fn handle_config_dl_req() -> impl IntoResponse {
Ok((headers, body))
}

/// Handles CSV file upload and parsing.
async fn handle_csv_upload(mut multipart: Multipart) -> impl IntoResponse {
while let Some(field) = multipart.next_field().await.unwrap() {
let file_name = field.file_name().unwrap().to_string();
let file_path = format!("/tmp/{}", file_name);
let mut file = tokio::fs::File::create(&file_path).await.unwrap();
while let Some(chunk) = field.chunk().await.unwrap() {
file.write_all(&chunk).await.unwrap();
}

match parse_csv_file(PathBuf::from(file_path)).await {
Ok(records) => {
for record in records {
println!("{:?}", record);
}
}
Err(err) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to parse CSV file: {err}"),
));
}
}
}

Ok(StatusCode::OK)
}

/// Fetches UFC Ripper's update information from the GitHub repo.
pub async fn get_latest_app_meta() -> anyhow::Result<JSON> {
let req_url = format!("{}/raw/master/package.json", get_app_metadata().repo);
Expand All @@ -196,7 +227,7 @@ pub async fn get_latest_app_meta() -> anyhow::Result<JSON> {
.await
.context("An error occurred while trying to retrieve app update information")?;

if !resp.status().is_success() {
if (!resp.status().is_success()) {
return Err(anyhow!(
"Server responded with an error for the app update check"
));
Expand All @@ -218,7 +249,7 @@ pub async fn get_media_tools_meta() -> anyhow::Result<JSON> {
.await
.context("An error occurred while trying to retrieve media-tools information")?;

if !resp.status().is_success() {
if (!resp.status().is_success()) {
return Err(anyhow!(
"Server responded with an error for the media-tools metadata request"
));
Expand All @@ -244,7 +275,7 @@ pub async fn download_media_tools(
.take();

for tool in tools {
if is_debug() {
if (is_debug()) {
println!("Downloading media tool - {tool}..\n");
}

Expand Down Expand Up @@ -288,7 +319,7 @@ pub async fn login_to_fight_pass(
pass: &str,
) -> anyhow::Result<LoginSession> {
let proxied_client = &*HTTP_PROXIED_CLIENT.load();
let client = if get_config().use_proxy {
let client = if (get_config().use_proxy) {
proxied_client
} else {
&*HTTP_CLIENT
Expand All @@ -309,11 +340,11 @@ pub async fn login_to_fight_pass(
.await
.context("An error occurred while trying to log into the Fight Pass")?;

if !resp.status().is_success() {
if (!resp.status().is_success()) {
let err_msg = "Login failed. Check your credentials and try again";
let resp_error_messages = get_messages_from_response(resp).await.context(err_msg)?;

if resp_error_messages.contains(&"badLocation".to_string()) {
if (resp_error_messages.contains(&"badLocation".to_string())) {
return Err(anyhow!(
"Login was blocked because of the IP address your UFC Ripper backend is bound to. \
Try disabling any active VPN connections, or use a proxy service (check configuration)"
Expand Down Expand Up @@ -342,12 +373,12 @@ pub async fn login_to_fight_pass(

/// Refreshes an expired access token and returns a new one.
pub async fn refresh_access_token() -> anyhow::Result<()> {
if is_debug() {
if (is_debug()) {
println!("Refreshing access token..\n");
}

let proxied_client = &*HTTP_PROXIED_CLIENT.load();
let client = if get_config().use_proxy {
let client = if (get_config().use_proxy) {
proxied_client
} else {
&*HTTP_CLIENT
Expand All @@ -364,16 +395,16 @@ pub async fn refresh_access_token() -> anyhow::Result<()> {
.await
.context("An error occurred while trying fetch VOD metadata")?;

if !resp.status().is_success() {
if (!resp.status().is_success()) {
let err_msg = "Failed to refresh your login session. Please login with your UFC Fight Pass account again";
let resp_error_messages = get_messages_from_response(resp).await.context(err_msg)?;

if resp_error_messages.contains(&"badLocation".to_string()) {
if (resp_error_messages.contains(&"badLocation".to_string())) {
return Err(anyhow!(
"Session refresh request was blocked because of the IP address your UFC Ripper backend is bound to. \
Try disabling any active VPN connections, or use a proxy service (check configuration)"
));
} else if resp_error_messages.contains(&"errorRefreshingToken".to_string()) {
} else if (resp_error_messages.contains(&"errorRefreshingToken".to_string())) {
return Err(anyhow!(
"Invalid refresh token. Please log in with your UFC Fight Pass account again"
));
Expand Down Expand Up @@ -404,7 +435,7 @@ pub async fn refresh_access_token() -> anyhow::Result<()> {
/// Searches the UFC Fight Pass library for VODs.
pub async fn search_vods(query: &str, page: u64) -> anyhow::Result<JSON> {
let proxied_client = &*HTTP_PROXIED_CLIENT.load();
let client = if get_config().use_proxy {
let client = if (get_config().use_proxy) {
proxied_client
} else {
&*HTTP_CLIENT
Expand All @@ -418,7 +449,7 @@ pub async fn search_vods(query: &str, page: u64) -> anyhow::Result<JSON> {
.append_pair("page", &page.to_string())
.append_pair(
"restrictSearchableAttributes",
if get_config().search_title_only {
if (get_config().search_title_only) {
r#"["name"]"#
} else {
"[]"
Expand All @@ -442,7 +473,7 @@ pub async fn search_vods(query: &str, page: u64) -> anyhow::Result<JSON> {
.await
.context("An error occurred while trying to search the Fight Pass library")?;

if !resp.status().is_success() {
if (!resp.status().is_success()) {
return Err(anyhow!(
"Server responded with an error for the search request"
));
Expand All @@ -455,7 +486,7 @@ pub async fn search_vods(query: &str, page: u64) -> anyhow::Result<JSON> {

let result = json_body.try_get("results").try_get(0);

if result == &JSON::Null {
if (result == &JSON::Null) {
Err(anyhow!("Response does not contain any search results"))
} else {
Ok(result.clone())
Expand All @@ -475,7 +506,7 @@ pub async fn get_vod_meta(url: &str) -> anyhow::Result<Vod> {
// Having this as a closure allows this process to be run multiple times.
let run_request = || async {
let proxied_client = &*HTTP_PROXIED_CLIENT.load();
let client = if get_config().use_proxy {
let client = if (get_config().use_proxy) {
proxied_client
} else {
&*HTTP_CLIENT
Expand All @@ -493,15 +524,15 @@ pub async fn get_vod_meta(url: &str) -> anyhow::Result<Vod> {

let status = resp.status();

if !status.is_success() {
if (!status.is_success()) {
let err_msg = "An unknown error occurred while trying fetch VOD metadata";

return match status.as_u16() {
401 => {
let resp_error_messages =
get_messages_from_response(resp).await.context(err_msg)?;

if resp_error_messages.contains(&"Bearer token is not valid".to_string()) {
if (resp_error_messages.contains(&"Bearer token is not valid".to_string())) {
Ok(ReqStatus::NeedsRefresh)
} else {
Err(anyhow!(
Expand Down Expand Up @@ -571,7 +602,7 @@ pub async fn get_vod_meta(url: &str) -> anyhow::Result<Vod> {
/// Fetches the HLS stream URL for a given Fight Pass video.
pub async fn get_vod_stream_url(vod_id: u64) -> anyhow::Result<String> {
let proxied_client = &*HTTP_PROXIED_CLIENT.load();
let client = if get_config().use_proxy {
let client = if (get_config().use_proxy) {
proxied_client
} else {
&*HTTP_CLIENT
Expand All @@ -587,7 +618,7 @@ pub async fn get_vod_stream_url(vod_id: u64) -> anyhow::Result<String> {
.await
.context("An error occurred while trying request the callback URL for VOD stream")?;

if !resp.status().is_success() {
if (!resp.status().is_success()) {
return Err(anyhow!(
"Server responded with an error to the callback URL request"
));
Expand All @@ -605,7 +636,7 @@ pub async fn get_vod_stream_url(vod_id: u64) -> anyhow::Result<String> {
.await
.context("An error occurred while trying request VOD stream URL")?;

if !resp.status().is_success() {
if (!resp.status().is_success()) {
return Err(anyhow!(
"Server responded with an error to the VOD stream request"
));
Expand Down
Loading