Refactor: Extract the quirks_path package as a standalone module and replace eyre with color-eyre.

This commit is contained in:
2025-01-05 23:51:31 +08:00
parent 40cbf86f0f
commit 2ed2b864b2
28 changed files with 231 additions and 2117 deletions

View File

@@ -1,107 +0,0 @@
name: CI
on:
push:
branches:
- master
- main
pull_request:
env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal
jobs:
rustfmt:
name: Check Style
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: ${{ env.TOOLCHAIN_PROFILE }}
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
components: rustfmt
- name: Run cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Run Clippy
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: ${{ env.TOOLCHAIN_PROFILE }}
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Run cargo clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms
test:
name: Run Tests
runs-on: ubuntu-latest
permissions:
contents: read
services:
redis:
image: redis
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- "6379:6379"
postgres:
image: postgres
env:
POSTGRES_DB: postgress_test
POSTGRES_USER: postgress
POSTGRES_PASSWORD: postgress
ports:
- "5432:5432"
# Set health checks to wait until postgres has started
options: --health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: ${{ env.TOOLCHAIN_PROFILE }}
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
args: --all-features --all
env:
REDIS_URL: redis://localhost:${{job.services.redis.ports[6379]}}
DATABASE_URL: postgres://postgress:postgress@localhost:5432/postgress_test

View File

@@ -22,11 +22,9 @@ testcontainers = [
]
[dependencies]
quirks_path = { path = "../../packages/quirks-path" }
loco-rs = { version = "0.13" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
eyre = "0.6"
tokio = { version = "1.42", features = ["macros", "fs", "rt-multi-thread"] }
async-trait = "0.1.83"
tracing = "0.1"
@@ -82,6 +80,9 @@ testcontainers = { version = "0.23.1", features = [
], optional = true }
testcontainers-modules = { version = "0.11.4", optional = true }
color-eyre = "0.6"
log = "0.4.22"
anyhow = "1.0.95"
bollard = { version = "0.18", optional = true }
@@ -89,7 +90,7 @@ async-graphql = { version = "7.0.13", features = [] }
async-graphql-axum = "7.0.13"
fastrand = "2.3.0"
seaography = "1.1.2"
quirks_path = "0.1.0"
[dev-dependencies]
serial_test = "3"

View File

@@ -1,5 +1,5 @@
#![allow(unused_imports)]
use eyre::Context;
use color_eyre::eyre::Context;
use itertools::Itertools;
use loco_rs::{
app::Hooks,
@@ -18,7 +18,8 @@ use recorder::{
};
use sea_orm_migration::MigratorTrait;
async fn pull_mikan_bangumi_rss(ctx: &AppContext) -> eyre::Result<()> {
async fn pull_mikan_bangumi_rss(ctx: &AppContext) -> color_eyre::eyre::Result<()> {
color_eyre::install()?;
let rss_link = "https://mikanani.me/RSS/Bangumi?bangumiId=3416&subgroupid=370";
// let rss_link =
@@ -47,7 +48,7 @@ async fn pull_mikan_bangumi_rss(ctx: &AppContext) -> eyre::Result<()> {
Ok(())
}
async fn init() -> eyre::Result<AppContext> {
async fn init() -> color_eyre::eyre::Result<AppContext> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_test_writer()
@@ -61,7 +62,7 @@ async fn init() -> eyre::Result<AppContext> {
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
async fn main() -> color_eyre::eyre::Result<()> {
let ctx = init().await?;
pull_mikan_bangumi_rss(&ctx).await?;

View File

@@ -2,7 +2,8 @@ use loco_rs::cli;
use recorder::{app::App, migrations::Migrator};
#[tokio::main]
async fn main() -> eyre::Result<()> {
async fn main() -> color_eyre::eyre::Result<()> {
color_eyre::install()?;
cli::main::<App, Migrator>().await?;
Ok(())
}

View File

@@ -87,7 +87,7 @@ impl AppDalClient {
bucket: Option<&str>,
filename: &str,
data: Bytes,
) -> eyre::Result<DalStoredUrl> {
) -> color_eyre::eyre::Result<DalStoredUrl> {
match content_category {
DalContentCategory::Image => {
let fullname = [
@@ -124,7 +124,7 @@ impl AppDalClient {
subscriber_pid: &str,
bucket: Option<&str>,
filename: &str,
) -> eyre::Result<Option<DalStoredUrl>> {
) -> color_eyre::eyre::Result<Option<DalStoredUrl>> {
match content_category {
DalContentCategory::Image => {
let fullname = [
@@ -158,7 +158,7 @@ impl AppDalClient {
subscriber_pid: &str,
bucket: Option<&str>,
filename: &str,
) -> eyre::Result<Buffer> {
) -> color_eyre::eyre::Result<Buffer> {
match content_category {
DalContentCategory::Image => {
let fullname = [

View File

@@ -166,7 +166,7 @@ pub fn build_mikan_bangumi_rss_link(
mikan_base_url: &str,
mikan_bangumi_id: &str,
mikan_fansub_id: Option<&str>,
) -> eyre::Result<Url> {
) -> color_eyre::eyre::Result<Url> {
let mut url = Url::parse(mikan_base_url)?;
url.set_path("/RSS/Bangumi");
url.query_pairs_mut()
@@ -181,7 +181,7 @@ pub fn build_mikan_bangumi_rss_link(
pub fn build_mikan_subscriber_aggregation_rss_link(
mikan_base_url: &str,
mikan_aggregation_id: &str,
) -> eyre::Result<Url> {
) -> color_eyre::eyre::Result<Url> {
let mut url = Url::parse(mikan_base_url)?;
url.set_path("/RSS/MyBangumi");
url.query_pairs_mut()
@@ -222,7 +222,7 @@ pub fn parse_mikan_subscriber_aggregation_id_from_rss_link(
pub async fn parse_mikan_rss_items_from_rss_link(
client: Option<&AppMikanClient>,
url: impl IntoUrl,
) -> eyre::Result<Vec<MikanRssItem>> {
) -> color_eyre::eyre::Result<Vec<MikanRssItem>> {
let channel = parse_mikan_rss_channel_from_rss_link(client, url).await?;
Ok(channel.into_items())
@@ -231,7 +231,7 @@ pub async fn parse_mikan_rss_items_from_rss_link(
pub async fn parse_mikan_rss_channel_from_rss_link(
client: Option<&AppMikanClient>,
url: impl IntoUrl,
) -> eyre::Result<MikanRssChannel> {
) -> color_eyre::eyre::Result<MikanRssChannel> {
let http_client = client.map(|s| s.deref());
let bytes = fetch_bytes(http_client, url.as_str()).await?;

View File

@@ -1,7 +1,7 @@
use std::ops::Deref;
use bytes::Bytes;
use eyre::ContextCompat;
use color_eyre::eyre::ContextCompat;
use html_escape::decode_html_entities;
use itertools::Itertools;
use lazy_static::lazy_static;
@@ -65,7 +65,7 @@ pub fn build_mikan_bangumi_homepage(
mikan_base_url: &str,
mikan_bangumi_id: &str,
mikan_fansub_id: Option<&str>,
) -> eyre::Result<Url> {
) -> color_eyre::eyre::Result<Url> {
let mut url = Url::parse(mikan_base_url)?;
url.set_path(&format!("/Home/Bangumi/{mikan_bangumi_id}"));
url.set_fragment(mikan_fansub_id);
@@ -75,7 +75,7 @@ pub fn build_mikan_bangumi_homepage(
pub fn build_mikan_episode_homepage(
mikan_base_url: &str,
mikan_episode_id: &str,
) -> eyre::Result<Url> {
) -> color_eyre::eyre::Result<Url> {
let mut url = Url::parse(mikan_base_url)?;
url.set_path(&format!("/Home/Episode/{mikan_episode_id}"));
Ok(url)
@@ -93,7 +93,7 @@ pub fn parse_mikan_episode_id_from_homepage(url: &Url) -> Option<MikanEpisodeHom
pub async fn parse_mikan_bangumi_poster_from_origin_poster_src(
client: Option<&AppMikanClient>,
origin_poster_src: Url,
) -> eyre::Result<MikanBangumiPosterMeta> {
) -> color_eyre::eyre::Result<MikanBangumiPosterMeta> {
let http_client = client.map(|s| s.deref());
let poster_data = fetch_image(http_client, origin_poster_src.clone()).await?;
Ok(MikanBangumiPosterMeta {
@@ -107,7 +107,7 @@ pub async fn parse_mikan_bangumi_poster_from_origin_poster_src_with_cache(
ctx: &AppContext,
origin_poster_src: Url,
subscriber_id: i32,
) -> eyre::Result<MikanBangumiPosterMeta> {
) -> color_eyre::eyre::Result<MikanBangumiPosterMeta> {
let dal_client = ctx.get_dal_client();
let mikan_client = ctx.get_mikan_client();
let subscriber_pid = &subscribers::Model::find_pid_by_id_with_cache(ctx, subscriber_id).await?;
@@ -149,7 +149,7 @@ pub async fn parse_mikan_bangumi_poster_from_origin_poster_src_with_cache(
pub async fn parse_mikan_bangumi_meta_from_mikan_homepage(
client: Option<&AppMikanClient>,
url: Url,
) -> eyre::Result<MikanBangumiMeta> {
) -> color_eyre::eyre::Result<MikanBangumiMeta> {
let http_client = client.map(|s| s.deref());
let url_host = url.origin().unicode_serialization();
let content = fetch_html(http_client, url.as_str()).await?;
@@ -272,7 +272,7 @@ pub async fn parse_mikan_bangumi_meta_from_mikan_homepage(
pub async fn parse_mikan_episode_meta_from_mikan_homepage(
client: Option<&AppMikanClient>,
url: Url,
) -> eyre::Result<MikanEpisodeMeta> {
) -> color_eyre::eyre::Result<MikanEpisodeMeta> {
let http_client = client.map(|s| s.deref());
let url_host = url.origin().unicode_serialization();
let content = fetch_html(http_client, url.as_str()).await?;
@@ -417,7 +417,7 @@ mod test {
#[tokio::test]
async fn test_parse_mikan_episode() {
let test_fn = async || -> eyre::Result<()> {
let test_fn = async || -> color_eyre::eyre::Result<()> {
let url_str =
"https://mikanani.me/Home/Episode/475184dce83ea2b82902592a5ac3343f6d54b36a";
let url = Url::parse(url_str)?;
@@ -461,7 +461,7 @@ mod test {
#[tokio::test]
async fn test_parse_mikan_bangumi() {
let test_fn = async || -> eyre::Result<()> {
let test_fn = async || -> color_eyre::eyre::Result<()> {
let url_str = "https://mikanani.me/Home/Bangumi/3416#370";
let url = Url::parse(url_str)?;

View File

@@ -68,7 +68,10 @@ fn replace_ch_bracket_to_en(raw_name: &str) -> String {
raw_name.replace('【', "[").replace('】', "]")
}
fn title_body_pre_process(title_body: &str, fansub: Option<&str>) -> eyre::Result<String> {
fn title_body_pre_process(
title_body: &str,
fansub: Option<&str>,
) -> color_eyre::eyre::Result<String> {
let raw_without_fansub = if let Some(fansub) = fansub {
let fan_sub_re = Regex::new(&format!(".{fansub}."))?;
fan_sub_re.replace_all(title_body, "")
@@ -256,7 +259,7 @@ pub fn check_is_movie(title: &str) -> bool {
MOVIE_TITLE_RE.is_match(title)
}
pub fn parse_episode_meta_from_raw_name(s: &str) -> eyre::Result<RawEpisodeMeta> {
pub fn parse_episode_meta_from_raw_name(s: &str) -> color_eyre::eyre::Result<RawEpisodeMeta> {
let raw_title = s.trim();
let raw_title_without_ch_brackets = replace_ch_bracket_to_en(raw_title);
let fansub = extract_fansub(&raw_title_without_ch_brackets);
@@ -309,7 +312,7 @@ pub fn parse_episode_meta_from_raw_name(s: &str) -> eyre::Result<RawEpisodeMeta>
resolution,
})
} else {
Err(eyre::eyre!(
Err(color_eyre::eyre::eyre!(
"Can not parse episode meta from raw filename {}",
raw_title
))

View File

@@ -1,4 +1,4 @@
use eyre::OptionExt;
use color_eyre::eyre::OptionExt;
use fancy_regex::Regex as FancyRegex;
use lazy_static::lazy_static;
use quirks_path::Path;
@@ -101,10 +101,10 @@ pub fn parse_episode_media_meta_from_torrent(
torrent_path: &Path,
torrent_name: Option<&str>,
season: Option<i32>,
) -> eyre::Result<TorrentEpisodeMediaMeta> {
) -> color_eyre::eyre::Result<TorrentEpisodeMediaMeta> {
let media_name = torrent_path
.file_name()
.ok_or_else(|| eyre::eyre!("failed to get file name of {}", torrent_path))?;
.ok_or_else(|| color_eyre::eyre::eyre!("failed to get file name of {}", torrent_path))?;
let mut match_obj = None;
for rule in TORRENT_EP_PARSE_RULES.iter() {
match_obj = if let Some(torrent_name) = torrent_name.as_ref() {
@@ -119,7 +119,7 @@ pub fn parse_episode_media_meta_from_torrent(
if let Some(match_obj) = match_obj {
let group_season_and_title = match_obj
.get(1)
.ok_or_else(|| eyre::eyre!("should have 1 group"))?
.ok_or_else(|| color_eyre::eyre::eyre!("should have 1 group"))?
.as_str();
let (fansub, season_and_title) = get_fansub(group_season_and_title);
let (title, season) = if let Some(season) = season {
@@ -146,7 +146,7 @@ pub fn parse_episode_media_meta_from_torrent(
extname,
})
} else {
Err(eyre::eyre!(
Err(color_eyre::eyre::eyre!(
"failed to parse episode media meta from torrent_path='{}' torrent_name='{:?}'",
torrent_path,
torrent_name
@@ -158,11 +158,11 @@ pub fn parse_episode_subtitle_meta_from_torrent(
torrent_path: &Path,
torrent_name: Option<&str>,
season: Option<i32>,
) -> eyre::Result<TorrentEpisodeSubtitleMeta> {
) -> color_eyre::eyre::Result<TorrentEpisodeSubtitleMeta> {
let media_meta = parse_episode_media_meta_from_torrent(torrent_path, torrent_name, season)?;
let media_name = torrent_path
.file_name()
.ok_or_else(|| eyre::eyre!("failed to get file name of {}", torrent_path))?;
.ok_or_else(|| color_eyre::eyre::eyre!("failed to get file name of {}", torrent_path))?;
let lang = get_subtitle_lang(media_name);

View File

@@ -3,7 +3,7 @@ use reqwest::IntoUrl;
use super::HttpClient;
pub async fn fetch_bytes<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<Bytes> {
pub async fn fetch_bytes<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> color_eyre::eyre::Result<Bytes> {
let client = client.unwrap_or_default();
let bytes = client

View File

@@ -2,7 +2,10 @@ use reqwest::IntoUrl;
use super::HttpClient;
pub async fn fetch_html<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<String> {
pub async fn fetch_html<T: IntoUrl>(
client: Option<&HttpClient>,
url: T,
) -> color_eyre::eyre::Result<String> {
let client = client.unwrap_or_default();
let content = client
.get(url)

View File

@@ -3,6 +3,6 @@ use reqwest::IntoUrl;
use super::{bytes::fetch_bytes, HttpClient};
pub async fn fetch_image<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> eyre::Result<Bytes> {
pub async fn fetch_image<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> color_eyre::eyre::Result<Bytes> {
fetch_bytes(client, url).await
}

View File

@@ -112,9 +112,9 @@ impl Model {
mikan_bangumi_id: String,
mikan_fansub_id: String,
f: F,
) -> eyre::Result<Model>
) -> color_eyre::eyre::Result<Model>
where
F: AsyncFnOnce(&mut ActiveModel) -> eyre::Result<()>,
F: AsyncFnOnce(&mut ActiveModel) -> color_eyre::eyre::Result<()>,
{
let db = &ctx.db;
if let Some(existed) = Entity::find()

View File

@@ -137,7 +137,7 @@ impl Model {
ctx: &AppContext,
subscription_id: i32,
creations: impl IntoIterator<Item = MikanEpsiodeCreation>,
) -> eyre::Result<()> {
) -> color_eyre::eyre::Result<()> {
let db = &ctx.db;
let new_episode_active_modes = creations
.into_iter()
@@ -187,7 +187,7 @@ impl ActiveModel {
pub fn from_mikan_episode_meta(
ctx: &AppContext,
creation: MikanEpsiodeCreation,
) -> eyre::Result<Self> {
) -> color_eyre::eyre::Result<Self> {
let item = creation.episode;
let bgm = creation.bangumi;
let raw_meta = parse_episode_meta_from_raw_name(&item.episode_title)

View File

@@ -128,7 +128,10 @@ impl Model {
subscriber.ok_or_else(|| ModelError::EntityNotFound)
}
pub async fn find_pid_by_id_with_cache(ctx: &AppContext, id: i32) -> eyre::Result<String> {
pub async fn find_pid_by_id_with_cache(
ctx: &AppContext,
id: i32,
) -> color_eyre::eyre::Result<String> {
let db = &ctx.db;
let cache = &ctx.cache;
let pid = cache

View File

@@ -182,7 +182,7 @@ impl Model {
ctx: &AppContext,
create_dto: SubscriptionCreateDto,
subscriber_id: i32,
) -> eyre::Result<Self> {
) -> color_eyre::eyre::Result<Self> {
let db = &ctx.db;
let subscription = ActiveModel::from_create_dto(create_dto, subscriber_id);
@@ -193,7 +193,7 @@ impl Model {
ctx: &AppContext,
ids: impl Iterator<Item = i32>,
enabled: bool,
) -> eyre::Result<()> {
) -> color_eyre::eyre::Result<()> {
let db = &ctx.db;
Entity::update_many()
.col_expr(Column::Enabled, Expr::value(enabled))
@@ -206,7 +206,7 @@ impl Model {
pub async fn delete_with_ids(
ctx: &AppContext,
ids: impl Iterator<Item = i32>,
) -> eyre::Result<()> {
) -> color_eyre::eyre::Result<()> {
let db = &ctx.db;
Entity::delete_many()
.filter(Column::Id.is_in(ids))
@@ -215,7 +215,7 @@ impl Model {
Ok(())
}
pub async fn pull_subscription(&self, ctx: &AppContext) -> eyre::Result<()> {
pub async fn pull_subscription(&self, ctx: &AppContext) -> color_eyre::eyre::Result<()> {
match &self.category {
SubscriptionCategory::Mikan => {
let mikan_client = ctx.get_mikan_client();
@@ -288,7 +288,7 @@ impl Model {
self.id,
mikan_bangumi_id.to_string(),
mikan_fansub_id.to_string(),
async |am| -> eyre::Result<()> {
async |am| -> color_eyre::eyre::Result<()> {
let bgm_meta = parse_mikan_bangumi_meta_from_mikan_homepage(
Some(mikan_client),
bgm_homepage.clone(),

View File

@@ -57,7 +57,7 @@ pub enum TorrentSource {
}
impl TorrentSource {
pub async fn parse(client: Option<&HttpClient>, url: &str) -> eyre::Result<Self> {
pub async fn parse(client: Option<&HttpClient>, url: &str) -> color_eyre::eyre::Result<Self> {
let url = Url::parse(url)?;
let source = if url.scheme() == MAGNET_SCHEMA {
TorrentSource::from_magnet_url(url)?
@@ -82,7 +82,10 @@ impl TorrentSource {
Ok(source)
}
pub fn from_torrent_file(file: Vec<u8>, name: Option<String>) -> eyre::Result<Self> {
pub fn from_torrent_file(
file: Vec<u8>,
name: Option<String>,
) -> color_eyre::eyre::Result<Self> {
let torrent: TorrentMetaV1Owned = torrent_from_bytes(&file)
.map_err(|_| TorrentDownloadError::InvalidTorrentFileFormat)?;
let hash = torrent.info_hash.as_string();
@@ -93,7 +96,7 @@ impl TorrentSource {
})
}
pub fn from_magnet_url(url: Url) -> eyre::Result<Self> {
pub fn from_magnet_url(url: Url) -> color_eyre::eyre::Result<Self> {
if url.scheme() != MAGNET_SCHEMA {
Err(TorrentDownloadError::InvalidUrlSchema {
found: url.scheme().to_string(),
@@ -117,7 +120,7 @@ impl TorrentSource {
}
}
pub fn from_torrent_url(url: Url, hash: String) -> eyre::Result<Self> {
pub fn from_torrent_url(url: Url, hash: String) -> color_eyre::eyre::Result<Self> {
Ok(TorrentSource::TorrentUrl { url, hash })
}
@@ -246,35 +249,47 @@ pub trait TorrentDownloader {
status_filter: TorrentFilter,
category: Option<String>,
tag: Option<String>,
) -> eyre::Result<Vec<Torrent>>;
) -> color_eyre::eyre::Result<Vec<Torrent>>;
async fn add_torrents(
&self,
source: TorrentSource,
save_path: String,
category: Option<&str>,
) -> eyre::Result<()>;
) -> color_eyre::eyre::Result<()>;
async fn delete_torrents(&self, hashes: Vec<String>) -> eyre::Result<()>;
async fn delete_torrents(&self, hashes: Vec<String>) -> color_eyre::eyre::Result<()>;
async fn rename_torrent_file(
&self,
hash: &str,
old_path: &str,
new_path: &str,
) -> eyre::Result<()>;
) -> color_eyre::eyre::Result<()>;
async fn move_torrents(&self, hashes: Vec<String>, new_path: &str) -> eyre::Result<()>;
async fn move_torrents(
&self,
hashes: Vec<String>,
new_path: &str,
) -> color_eyre::eyre::Result<()>;
async fn get_torrent_path(&self, hashes: String) -> eyre::Result<Option<String>>;
async fn get_torrent_path(&self, hashes: String) -> color_eyre::eyre::Result<Option<String>>;
async fn check_connection(&self) -> eyre::Result<()>;
async fn check_connection(&self) -> color_eyre::eyre::Result<()>;
async fn set_torrents_category(&self, hashes: Vec<String>, category: &str) -> eyre::Result<()>;
async fn set_torrents_category(
&self,
hashes: Vec<String>,
category: &str,
) -> color_eyre::eyre::Result<()>;
async fn add_torrent_tags(&self, hashes: Vec<String>, tags: Vec<String>) -> eyre::Result<()>;
async fn add_torrent_tags(
&self,
hashes: Vec<String>,
tags: Vec<String>,
) -> color_eyre::eyre::Result<()>;
async fn add_category(&self, category: &str) -> eyre::Result<()>;
async fn add_category(&self, category: &str) -> color_eyre::eyre::Result<()>;
fn get_save_path(&self, sub_path: &Path) -> PathBuf;
}

View File

@@ -1,6 +1,7 @@
pub mod core;
pub mod error;
pub mod qbit;
mod utils;
pub use core::{
Torrent, TorrentContent, TorrentDownloader, TorrentFilter, TorrentSource, BITTORRENT_MIME_TYPE,

View File

@@ -3,7 +3,7 @@ use std::{
};
use async_trait::async_trait;
use eyre::OptionExt;
use color_eyre::eyre::OptionExt;
use futures::future::try_join_all;
pub use qbit_rs::model::{
Torrent as QbitTorrent, TorrentContent as QbitTorrentContent, TorrentFile as QbitTorrentFile,
@@ -13,12 +13,15 @@ use qbit_rs::{
model::{AddTorrentArg, Credential, GetTorrentListArg, NonEmptyStr, SyncData},
Qbit,
};
use quirks_path::{path_equals_as_file_url, Path, PathBuf};
use quirks_path::{Path, PathBuf};
use tokio::time::sleep;
use tracing::instrument;
use url::Url;
use super::{Torrent, TorrentDownloadError, TorrentDownloader, TorrentFilter, TorrentSource};
use super::{
utils::path_equals_as_file_url, Torrent, TorrentDownloadError, TorrentDownloader,
TorrentFilter, TorrentSource,
};
impl From<TorrentSource> for QbitTorrentSource {
fn from(value: TorrentSource) -> Self {
@@ -105,7 +108,7 @@ impl QBittorrentDownloader {
}
#[instrument(level = "debug")]
pub async fn api_version(&self) -> eyre::Result<String> {
pub async fn api_version(&self) -> color_eyre::eyre::Result<String> {
let result = self.client.get_webapi_version().await?;
Ok(result)
}
@@ -116,11 +119,11 @@ impl QBittorrentDownloader {
fetch_data_fn: G,
mut stop_wait_fn: F,
timeout: Option<Duration>,
) -> eyre::Result<()>
) -> color_eyre::eyre::Result<()>
where
H: FnOnce() -> E,
G: Fn(Arc<Qbit>, E) -> Fut,
Fut: Future<Output = eyre::Result<D>>,
Fut: Future<Output = color_eyre::eyre::Result<D>>,
F: FnMut(&D) -> bool,
E: Clone,
D: Debug + serde::Serialize,
@@ -161,7 +164,7 @@ impl QBittorrentDownloader {
arg: GetTorrentListArg,
stop_wait_fn: F,
timeout: Option<Duration>,
) -> eyre::Result<()>
) -> color_eyre::eyre::Result<()>
where
F: FnMut(&Vec<QbitTorrent>) -> bool,
{
@@ -169,7 +172,7 @@ impl QBittorrentDownloader {
|| arg,
async move |client: Arc<Qbit>,
arg: GetTorrentListArg|
-> eyre::Result<Vec<QbitTorrent>> {
-> color_eyre::eyre::Result<Vec<QbitTorrent>> {
let data = client.get_torrent_list(arg).await?;
Ok(data)
},
@@ -184,10 +187,10 @@ impl QBittorrentDownloader {
&self,
stop_wait_fn: F,
timeout: Option<Duration>,
) -> eyre::Result<()> {
) -> color_eyre::eyre::Result<()> {
self.wait_until(
|| (),
async move |client: Arc<Qbit>, _| -> eyre::Result<SyncData> {
async move |client: Arc<Qbit>, _| -> color_eyre::eyre::Result<SyncData> {
let data = client.sync(None).await?;
Ok(data)
},
@@ -203,12 +206,12 @@ impl QBittorrentDownloader {
hash: &str,
stop_wait_fn: F,
timeout: Option<Duration>,
) -> eyre::Result<()> {
) -> color_eyre::eyre::Result<()> {
self.wait_until(
|| Arc::new(hash.to_string()),
async move |client: Arc<Qbit>,
hash_arc: Arc<String>|
-> eyre::Result<Vec<QbitTorrentContent>> {
-> color_eyre::eyre::Result<Vec<QbitTorrentContent>> {
let data = client.get_torrent_contents(hash_arc.as_str(), None).await?;
Ok(data)
},
@@ -227,7 +230,7 @@ impl TorrentDownloader for QBittorrentDownloader {
status_filter: TorrentFilter,
category: Option<String>,
tag: Option<String>,
) -> eyre::Result<Vec<Torrent>> {
) -> color_eyre::eyre::Result<Vec<Torrent>> {
let arg = GetTorrentListArg {
filter: Some(status_filter.into()),
category,
@@ -256,7 +259,7 @@ impl TorrentDownloader for QBittorrentDownloader {
source: TorrentSource,
save_path: String,
category: Option<&str>,
) -> eyre::Result<()> {
) -> color_eyre::eyre::Result<()> {
let arg = AddTorrentArg {
source: source.clone().into(),
savepath: Some(save_path),
@@ -290,7 +293,7 @@ impl TorrentDownloader for QBittorrentDownloader {
}
#[instrument(level = "debug", skip(self))]
async fn delete_torrents(&self, hashes: Vec<String>) -> eyre::Result<()> {
async fn delete_torrents(&self, hashes: Vec<String>) -> color_eyre::eyre::Result<()> {
self.client
.delete_torrents(hashes.clone(), Some(true))
.await?;
@@ -311,7 +314,7 @@ impl TorrentDownloader for QBittorrentDownloader {
hash: &str,
old_path: &str,
new_path: &str,
) -> eyre::Result<()> {
) -> color_eyre::eyre::Result<()> {
self.client.rename_file(hash, old_path, new_path).await?;
let new_path = self.save_path.join(new_path);
let save_path = self.save_path.as_path();
@@ -333,7 +336,11 @@ impl TorrentDownloader for QBittorrentDownloader {
}
#[instrument(level = "debug", skip(self))]
async fn move_torrents(&self, hashes: Vec<String>, new_path: &str) -> eyre::Result<()> {
async fn move_torrents(
&self,
hashes: Vec<String>,
new_path: &str,
) -> color_eyre::eyre::Result<()> {
self.client
.set_torrent_location(hashes.clone(), new_path)
.await?;
@@ -357,7 +364,7 @@ impl TorrentDownloader for QBittorrentDownloader {
Ok(())
}
async fn get_torrent_path(&self, hashes: String) -> eyre::Result<Option<String>> {
async fn get_torrent_path(&self, hashes: String) -> color_eyre::eyre::Result<Option<String>> {
let mut torrent_list = self
.client
.get_torrent_list(GetTorrentListArg {
@@ -370,13 +377,17 @@ impl TorrentDownloader for QBittorrentDownloader {
}
#[instrument(level = "debug", skip(self))]
async fn check_connection(&self) -> eyre::Result<()> {
async fn check_connection(&self) -> color_eyre::eyre::Result<()> {
self.api_version().await?;
Ok(())
}
#[instrument(level = "debug", skip(self))]
async fn set_torrents_category(&self, hashes: Vec<String>, category: &str) -> eyre::Result<()> {
async fn set_torrents_category(
&self,
hashes: Vec<String>,
category: &str,
) -> color_eyre::eyre::Result<()> {
let result = self
.client
.set_torrent_category(hashes.clone(), category)
@@ -405,9 +416,13 @@ impl TorrentDownloader for QBittorrentDownloader {
}
#[instrument(level = "debug", skip(self))]
async fn add_torrent_tags(&self, hashes: Vec<String>, tags: Vec<String>) -> eyre::Result<()> {
async fn add_torrent_tags(
&self,
hashes: Vec<String>,
tags: Vec<String>,
) -> color_eyre::eyre::Result<()> {
if tags.is_empty() {
return Err(eyre::eyre!("add torrent tags can not be empty"));
return Err(color_eyre::eyre::eyre!("add torrent tags can not be empty"));
}
self.client
.add_torrent_tags(hashes.clone(), tags.clone())
@@ -435,7 +450,7 @@ impl TorrentDownloader for QBittorrentDownloader {
}
#[instrument(level = "debug", skip(self))]
async fn add_category(&self, category: &str) -> eyre::Result<()> {
async fn add_category(&self, category: &str) -> color_eyre::eyre::Result<()> {
self.client
.add_category(
NonEmptyStr::new(category).ok_or_eyre("category can not be empty")?,
@@ -486,7 +501,8 @@ pub mod tests {
#[cfg(feature = "testcontainers")]
pub async fn create_qbit_testcontainer(
) -> eyre::Result<testcontainers::ContainerRequest<testcontainers::GenericImage>> {
) -> color_eyre::eyre::Result<testcontainers::ContainerRequest<testcontainers::GenericImage>>
{
use testcontainers::{
core::{
ContainerPort,
@@ -522,7 +538,7 @@ pub mod tests {
#[cfg(feature = "testcontainers")]
#[tokio::test(flavor = "multi_thread")]
async fn test_qbittorrent_downloader() -> eyre::Result<()> {
async fn test_qbittorrent_downloader() -> color_eyre::eyre::Result<()> {
use testcontainers::runners::AsyncRunner;
use tokio::io::AsyncReadExt;
@@ -573,7 +589,7 @@ pub mod tests {
async fn test_qbittorrent_downloader_impl(
username: Option<&str>,
password: Option<&str>,
) -> eyre::Result<()> {
) -> color_eyre::eyre::Result<()> {
let base_save_path = Path::new(get_tmp_qbit_test_folder());
let mut downloader = QBittorrentDownloader::from_creation(QBittorrentDownloaderCreation {
@@ -607,7 +623,7 @@ pub mod tests {
.add_torrents(torrent_source, save_path.to_string(), Some("bangumi"))
.await?;
let get_torrent = async || -> eyre::Result<Torrent> {
let get_torrent = async || -> color_eyre::eyre::Result<Torrent> {
let torrent_infos = downloader
.get_torrents_info(TorrentFilter::All, None, None)
.await?;

View File

@@ -0,0 +1,11 @@
use quirks_path::{Path, PathToUrlError};
pub fn path_equals_as_file_url<A: AsRef<Path>, B: AsRef<Path>>(
a: A,
b: B,
) -> Result<bool, PathToUrlError> {
let u1 = a.as_ref().to_file_url()?;
let u2 = b.as_ref().to_file_url()?;
Ok(u1.as_str() == u2.as_str())
}

View File

@@ -19,7 +19,7 @@ where
container_label: &str,
prune: bool,
force: bool,
) -> eyre::Result<Self>;
) -> color_eyre::eyre::Result<Self>;
fn with_default_log_consumer(self) -> Self;
}
@@ -34,7 +34,7 @@ where
container_label: &str,
prune: bool,
force: bool,
) -> eyre::Result<Self> {
) -> color_eyre::eyre::Result<Self> {
use std::collections::HashMap;
use bollard::container::PruneContainersOptions;