Add a Bitmagnet import action
This commit is contained in:
parent
8eda807ead
commit
d26931f4c6
15 changed files with 402 additions and 10 deletions
70
src/actions/bitmagnet/action.rs
Normal file
70
src/actions/bitmagnet/action.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use crate::actions::action::{Action, ProcessedMagnets};
|
||||
use crate::actions::bitmagnet::client::BitmagnetClient;
|
||||
use crate::actions::bitmagnet::config::BitmagnetConfig;
|
||||
use crate::db::{BitmagnetProcessedTable, Database};
|
||||
use color_eyre::eyre::Result;
|
||||
use log::{debug, warn};
|
||||
|
||||
/// Action for submitting magnet links to Bitmagnet
|
||||
pub struct BitmagnetAction {
|
||||
client: BitmagnetClient,
|
||||
}
|
||||
|
||||
impl BitmagnetAction {
|
||||
pub async fn new(config: &BitmagnetConfig) -> Result<Self> {
|
||||
let client = BitmagnetClient::new(config)?;
|
||||
|
||||
Ok(BitmagnetAction { client })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Action for BitmagnetAction {
|
||||
/// Return the name of the action
|
||||
fn name(&self) -> &str {
|
||||
"Bitmagnet"
|
||||
}
|
||||
|
||||
/// Process all unprocessed magnet links and return the list of processed magnets
|
||||
async fn process_unprocessed_magnets(&mut self, db: &mut Database) -> Result<ProcessedMagnets> {
|
||||
let unprocessed_magnets =
|
||||
db.get_unprocessed_magnets_for_table::<BitmagnetProcessedTable>()?;
|
||||
let mut processed_magnets = Vec::new();
|
||||
let mut failed_magnets = Vec::new();
|
||||
|
||||
for magnet in unprocessed_magnets {
|
||||
if let Some(id) = magnet.id {
|
||||
match self
|
||||
.client
|
||||
.submit_magnet(
|
||||
&magnet.link,
|
||||
&magnet.published_at.and_utc(),
|
||||
&magnet.imdb_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
debug!(
|
||||
"Successfully submitted magnet link to Bitmagnet: {}",
|
||||
magnet.title
|
||||
);
|
||||
debug!("Magnet link: {}", magnet.link);
|
||||
db.mark_magnet_processed_for_table::<BitmagnetProcessedTable>(id)?;
|
||||
processed_magnets.push(magnet);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to submit magnet link to Bitmagnet: {}", e);
|
||||
failed_magnets.push(magnet);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Skipping magnet with null ID: {}", magnet.link);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ProcessedMagnets {
|
||||
success: processed_magnets,
|
||||
failed: failed_magnets,
|
||||
})
|
||||
}
|
||||
}
|
||||
129
src/actions/bitmagnet/client.rs
Normal file
129
src/actions/bitmagnet/client.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
use crate::actions::bitmagnet::config::BitmagnetConfig;
|
||||
use chrono::{DateTime, Utc};
|
||||
use color_eyre::eyre::{eyre, Result, WrapErr};
|
||||
use log::{info, warn};
|
||||
use magnet_url::Magnet;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use url::Url;
|
||||
use urlencoding::decode;
|
||||
|
||||
/// High-level Bitmagnet client
|
||||
pub struct BitmagnetClient {
|
||||
client: Client,
|
||||
base_url: Url,
|
||||
}
|
||||
|
||||
impl BitmagnetClient {
|
||||
/// Create a new Bitmagnet client from configuration
|
||||
pub fn new(config: &BitmagnetConfig) -> Result<Self> {
|
||||
if !config.enable {
|
||||
return Err(eyre!("Bitmagnet action is disabled"));
|
||||
}
|
||||
|
||||
let url_str = &config.host;
|
||||
let base_url = Url::parse(url_str).wrap_err_with(|| format!("Invalid URL: {}", url_str))?;
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
Ok(BitmagnetClient { client, base_url })
|
||||
}
|
||||
|
||||
/// Submit a magnet link to Bitmagnet
|
||||
pub async fn submit_magnet(
|
||||
&self,
|
||||
magnet: &str,
|
||||
published_at: &DateTime<Utc>,
|
||||
imdb_id: &Option<String>,
|
||||
) -> Result<()> {
|
||||
let url = self
|
||||
.base_url
|
||||
.join("import")
|
||||
.wrap_err("Failed to construct API URL")?;
|
||||
|
||||
let structured_magnet =
|
||||
Magnet::new(magnet).map_err(|e| eyre!("Invalid magnet link: {:?}", e))?;
|
||||
|
||||
// Create a JSON object with the required fields
|
||||
let mut json_body = json!({
|
||||
"publishedAt": published_at.to_rfc3339(),
|
||||
"source": "reddit-magnet",
|
||||
});
|
||||
|
||||
match structured_magnet.xt {
|
||||
Some(info_hash) => {
|
||||
json_body["infoHash"] = json!(info_hash);
|
||||
}
|
||||
None => {
|
||||
return Err(eyre!("Info hash not found in magnet link"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = structured_magnet.dn {
|
||||
json_body["name"] = json!(decode(&*name)?);
|
||||
}
|
||||
|
||||
if let Some(id) = imdb_id {
|
||||
json_body["contentSource"] = json!("imdb");
|
||||
json_body["contentId"] = json!(id);
|
||||
}
|
||||
|
||||
if let Some(size) = structured_magnet.xl {
|
||||
json_body["size"] = json!(size);
|
||||
}
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(url)
|
||||
.json(&json_body)
|
||||
.send()
|
||||
.await
|
||||
.wrap_err("Failed to send request to Bitmagnet API")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
return Err(eyre!(
|
||||
"Bitmagnet API returned error: {} - {}",
|
||||
status,
|
||||
error_text
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the info hash from a magnet link
|
||||
fn extract_info_hash(magnet: &str) -> Result<String> {
|
||||
// Magnet links typically have the format: magnet:?xt=urn:btih:<info_hash>&dn=<name>&...
|
||||
let parts: Vec<&str> = magnet.split('&').collect();
|
||||
|
||||
for part in parts {
|
||||
if part.starts_with("magnet:?xt=urn:btih:") {
|
||||
return Ok(part[20..].to_string());
|
||||
}
|
||||
if part.starts_with("xt=urn:btih:") {
|
||||
return Ok(part[12..].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err(eyre!("Info hash not found in magnet link"))
|
||||
}
|
||||
|
||||
/// Extract the name from a magnet link
|
||||
fn extract_name(magnet: &str) -> Option<String> {
|
||||
// Magnet links typically have the format: magnet:?xt=urn:btih:<info_hash>&dn=<name>&...
|
||||
let parts: Vec<&str> = magnet.split('&').collect();
|
||||
|
||||
for part in parts {
|
||||
if part.starts_with("dn=") {
|
||||
return Some(part[3..].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
8
src/actions/bitmagnet/config.rs
Normal file
8
src/actions/bitmagnet/config.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Configuration for the Bitmagnet action
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BitmagnetConfig {
|
||||
pub enable: bool,
|
||||
pub host: String,
|
||||
}
|
||||
7
src/actions/bitmagnet/mod.rs
Normal file
7
src/actions/bitmagnet/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
mod action;
|
||||
mod client;
|
||||
mod config;
|
||||
|
||||
pub use action::BitmagnetAction;
|
||||
pub use client::BitmagnetClient;
|
||||
pub use config::BitmagnetConfig;
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod action;
|
||||
pub mod bitmagnet;
|
||||
pub mod transmission;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ impl Action for TransmissionAction {
|
|||
}
|
||||
} else {
|
||||
warn!("Skipping magnet with null ID: {}", magnet.link);
|
||||
// Consider adding to failed_magnets if we want to report these as well
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::actions::bitmagnet::BitmagnetConfig;
|
||||
use crate::actions::transmission::TransmissionConfig;
|
||||
use crate::args::Args;
|
||||
use color_eyre::eyre::{eyre, Result, WrapErr};
|
||||
|
|
@ -24,6 +25,9 @@ pub struct SourceConfig {
|
|||
/// Main application configuration
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub bitmagnet: Option<BitmagnetConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub transmission: Option<TransmissionConfig>,
|
||||
|
||||
|
|
|
|||
29
src/db.rs
29
src/db.rs
|
|
@ -1,5 +1,5 @@
|
|||
use crate::models::{Magnet, NewMagnet, NewTransmissionProcessed};
|
||||
use crate::schema::{magnets, transmission_processed};
|
||||
use crate::models::{Magnet, NewBitmagnetProcessed, NewMagnet, NewTransmissionProcessed};
|
||||
use crate::schema::{bitmagnet_processed, magnets, transmission_processed};
|
||||
use crate::PostInfo;
|
||||
use color_eyre::eyre::{eyre, Result, WrapErr};
|
||||
use diesel::prelude::*;
|
||||
|
|
@ -16,8 +16,33 @@ pub trait ProcessedTable {
|
|||
fn mark_processed(conn: &mut SqliteConnection, magnet_id: i32) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct BitmagnetProcessedTable;
|
||||
pub struct TransmissionProcessedTable;
|
||||
|
||||
impl ProcessedTable for BitmagnetProcessedTable {
|
||||
fn get_processed_ids(conn: &mut SqliteConnection) -> Result<Vec<i32>> {
|
||||
bitmagnet_processed::table
|
||||
.select(bitmagnet_processed::magnet_id)
|
||||
.load(conn)
|
||||
.wrap_err("Failed to load processed magnet IDs for Bitmagnet")
|
||||
}
|
||||
|
||||
fn mark_processed(conn: &mut SqliteConnection, magnet_id: i32) -> Result<()> {
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
let new_processed = NewBitmagnetProcessed {
|
||||
magnet_id,
|
||||
processed_at: &now,
|
||||
};
|
||||
|
||||
diesel::insert_into(bitmagnet_processed::table)
|
||||
.values(&new_processed)
|
||||
.execute(conn)
|
||||
.wrap_err("Failed to mark magnet as processed by Bitmagnet")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessedTable for TransmissionProcessedTable {
|
||||
fn get_processed_ids(conn: &mut SqliteConnection) -> Result<Vec<i32>> {
|
||||
transmission_processed::table
|
||||
|
|
|
|||
20
src/main.rs
20
src/main.rs
|
|
@ -1,4 +1,5 @@
|
|||
use crate::actions::action::Action;
|
||||
use crate::actions::bitmagnet::BitmagnetAction;
|
||||
use crate::actions::transmission::TransmissionAction;
|
||||
use crate::args::Args;
|
||||
use crate::config::{get_db_path, load_config};
|
||||
|
|
@ -133,6 +134,25 @@ async fn main() -> Result<()> {
|
|||
// Initialize actions
|
||||
let mut actions: Vec<Box<dyn Action>> = Vec::new();
|
||||
|
||||
// Add Bitmagnet action if enabled
|
||||
if let Some(bitmagnet_config) = conf.bitmagnet {
|
||||
if bitmagnet_config.enable {
|
||||
info!("Initializing Bitmagnet action");
|
||||
match BitmagnetAction::new(&bitmagnet_config).await {
|
||||
Ok(bitmagnet_action) => {
|
||||
actions.push(Box::new(bitmagnet_action));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to initialize Bitmagnet action: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Bitmagnet action is disabled");
|
||||
}
|
||||
} else {
|
||||
debug!("No Bitmagnet configuration found");
|
||||
}
|
||||
|
||||
// Add Transmission action if enabled
|
||||
if let Some(transmission_config) = conf.transmission {
|
||||
if transmission_config.enable {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::schema::{magnets, transmission_processed};
|
||||
use crate::schema::{bitmagnet_processed, magnets, transmission_processed};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::prelude::*;
|
||||
|
||||
|
|
@ -27,6 +27,23 @@ pub struct NewMagnet<'a> {
|
|||
pub imdb_id: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable)]
|
||||
#[diesel(table_name = bitmagnet_processed)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct BitmagnetProcessed {
|
||||
pub id: Option<i32>,
|
||||
pub magnet_id: i32,
|
||||
pub processed_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = bitmagnet_processed)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub struct NewBitmagnetProcessed<'a> {
|
||||
pub magnet_id: i32,
|
||||
pub processed_at: &'a NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable)]
|
||||
#[diesel(table_name = transmission_processed)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
bitmagnet_processed (id) {
|
||||
id -> Nullable<Integer>,
|
||||
magnet_id -> Integer,
|
||||
processed_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
magnets (id) {
|
||||
id -> Nullable<Integer>,
|
||||
|
|
@ -20,6 +28,7 @@ diesel::table! {
|
|||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(bitmagnet_processed -> magnets (magnet_id));
|
||||
diesel::joinable!(transmission_processed -> magnets (magnet_id));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(magnets, transmission_processed,);
|
||||
diesel::allow_tables_to_appear_in_same_query!(bitmagnet_processed, magnets, transmission_processed,);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue