Refactor: Extract the quirks_path package as a standalone module and replace eyre with color-eyre.
This commit is contained in:
parent
40cbf86f0f
commit
2ed2b864b2
99
Cargo.lock
generated
99
Cargo.lock
generated
@ -14,13 +14,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
@ -534,17 +540,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.74"
|
||||
version = "0.3.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
|
||||
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.7.4",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -870,6 +876,16 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_rust_features"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e2a7913e334891bf80b781c61a6cafce022f6f021d81151f8d11edc9697d72b"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.39"
|
||||
@ -957,6 +973,33 @@ version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "color-eyre"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"color-spantrace",
|
||||
"eyre",
|
||||
"indenter",
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-spantrace"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-core",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
@ -1744,7 +1787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
"miniz_oxide 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1978,9 +2021,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
@ -3445,6 +3488,15 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.2"
|
||||
@ -3649,9 +3701,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@ -3825,6 +3877,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
||||
|
||||
[[package]]
|
||||
name = "parcel_selectors"
|
||||
version = "0.28.0"
|
||||
@ -4442,12 +4500,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "quirks_path"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6437134ea524dd9b37c6ce5bebb35721607f64aa50f71615f8f5a68081162a0"
|
||||
dependencies = [
|
||||
"cfg_rust_features",
|
||||
"nom",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"thiserror 2.0.9",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -4544,7 +4603,7 @@ dependencies = [
|
||||
"bollard",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"eyre",
|
||||
"color-eyre",
|
||||
"fancy-regex",
|
||||
"fastrand",
|
||||
"figment",
|
||||
@ -6725,6 +6784,16 @@ dependencies = [
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-error"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
|
@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["apps/recorder", "packages/quirks-path"]
|
||||
members = ["apps/recorder"]
|
||||
resolver = "2"
|
||||
|
||||
[patch.crates-io]
|
||||
|
@ -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"
|
||||
|
@ -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?;
|
||||
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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 = [
|
||||
|
@ -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?;
|
||||
|
||||
|
@ -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)?;
|
||||
|
||||
|
@ -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
|
||||
))
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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?;
|
||||
|
11
apps/recorder/src/sync/utils.rs
Normal file
11
apps/recorder/src/sync/utils.rs
Normal 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())
|
||||
}
|
@ -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;
|
||||
|
@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "quirks_path"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nom = "7.1.3"
|
||||
percent-encoding = "2.3.1"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
thiserror = "1.0.57"
|
||||
tracing = "0.1.41"
|
||||
url = "2.5.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tracing = "0.1.41"
|
File diff suppressed because it is too large
Load Diff
@ -1,141 +0,0 @@
|
||||
use std::{borrow::Cow, fmt::Write};
|
||||
|
||||
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
|
||||
|
||||
use crate::{windows::parse_drive, Component, Path, Prefix};
|
||||
|
||||
const URL_FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
|
||||
const URL_PATH: &AsciiSet = &URL_FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
|
||||
const URL_PATH_SEGMENT: &AsciiSet = &URL_PATH.add(b'/').add(b'%');
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum PathToUrlError {
|
||||
#[error(transparent)]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
#[error("PathNotAbsoluteError {{ path = {path} }}")]
|
||||
PathNotAbsoluteError { path: Cow<'static, str> },
|
||||
#[error("NotSupportedPrefixError {{ path = {path}, prefix = {prefix} }}")]
|
||||
NotSupportedPrefixError {
|
||||
path: Cow<'static, str>,
|
||||
prefix: Cow<'static, str>,
|
||||
},
|
||||
#[error("NotSupportedFirstComponentError {{ path = {path}, comp = {comp} }}")]
|
||||
NotSupportedFirstComponentError {
|
||||
path: Cow<'static, str>,
|
||||
comp: Cow<'static, str>,
|
||||
},
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_u32(i: usize) -> Result<u32, url::ParseError> {
|
||||
if i <= u32::MAX as usize {
|
||||
Ok(i as u32)
|
||||
} else {
|
||||
Err(url::ParseError::Overflow)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn path_to_file_url_segments(
|
||||
path: &Path,
|
||||
serialization: &mut String,
|
||||
) -> Result<(u32, Option<::url::Host<String>>), PathToUrlError> {
|
||||
if !path.is_absolute() {
|
||||
return Err(PathToUrlError::PathNotAbsoluteError {
|
||||
path: Cow::Owned(path.as_str().to_string()),
|
||||
});
|
||||
}
|
||||
let mut components = path.components();
|
||||
|
||||
let host_start = serialization.len() + 1;
|
||||
let host_end;
|
||||
let host_internal: Option<url::Host<String>>;
|
||||
|
||||
match components.next() {
|
||||
Some(Component::Prefix(ref p)) => match p.kind() {
|
||||
Prefix::Disk { drive } | Prefix::VerbatimDisk { drive } => {
|
||||
host_end = to_u32(serialization.len()).unwrap();
|
||||
host_internal = None;
|
||||
serialization.push('/');
|
||||
serialization.push(drive);
|
||||
serialization.push(':');
|
||||
}
|
||||
Prefix::UNC { server, share } | Prefix::VerbatimUNC { server, share } => {
|
||||
let host = url::Host::parse(server)?;
|
||||
write!(serialization, "{}", host).unwrap();
|
||||
host_end = to_u32(serialization.len()).unwrap();
|
||||
host_internal = Some(host);
|
||||
serialization.push('/');
|
||||
serialization.extend(percent_encode(share.as_bytes(), URL_PATH_SEGMENT));
|
||||
}
|
||||
_ => {
|
||||
return Err(PathToUrlError::NotSupportedPrefixError {
|
||||
path: Cow::Owned(path.as_str().to_string()),
|
||||
prefix: Cow::Owned(p.as_str().to_string()),
|
||||
});
|
||||
}
|
||||
},
|
||||
Some(Component::RootDir(_)) => {
|
||||
let host_end = to_u32(serialization.len()).unwrap();
|
||||
let mut empty = true;
|
||||
for component in components {
|
||||
empty = false;
|
||||
serialization.push('/');
|
||||
|
||||
serialization.extend(percent_encode(
|
||||
component.as_str().as_bytes(),
|
||||
URL_PATH_SEGMENT,
|
||||
));
|
||||
}
|
||||
|
||||
if empty {
|
||||
serialization.push('/');
|
||||
}
|
||||
return Ok((host_end, None));
|
||||
}
|
||||
Some(comp) => {
|
||||
return Err(PathToUrlError::NotSupportedFirstComponentError {
|
||||
path: Cow::Owned(path.as_str().to_string()),
|
||||
comp: Cow::Owned(comp.as_str().to_string()),
|
||||
});
|
||||
}
|
||||
None => {
|
||||
return Err(PathToUrlError::NotSupportedFirstComponentError {
|
||||
path: Cow::Owned(path.as_str().to_string()),
|
||||
comp: Cow::Borrowed("null"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut path_only_has_prefix = true;
|
||||
for component in components {
|
||||
if matches!(component, Component::RootDir(..)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
path_only_has_prefix = false;
|
||||
let component = component.as_str();
|
||||
|
||||
serialization.push('/');
|
||||
serialization.extend(percent_encode(component.as_bytes(), URL_PATH_SEGMENT));
|
||||
}
|
||||
|
||||
// A windows drive letter must end with a slash.
|
||||
if serialization.len() > host_start
|
||||
&& matches!(parse_drive(&serialization[host_start..]), Ok(..))
|
||||
&& path_only_has_prefix
|
||||
{
|
||||
serialization.push('/');
|
||||
}
|
||||
|
||||
Ok((host_end, host_internal))
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
use nom::{
|
||||
bytes::complete::tag,
|
||||
character::complete::{self, satisfy},
|
||||
combinator::peek,
|
||||
error::{context, ContextError, Error, ErrorKind, ParseError},
|
||||
sequence::pair,
|
||||
AsChar, IResult, InputIter, InputTakeAtPosition,
|
||||
};
|
||||
|
||||
use crate::Prefix;
|
||||
|
||||
fn non_slash(input: &str) -> IResult<&str, &str> {
|
||||
input.split_at_position_complete(|item| item != '/')
|
||||
}
|
||||
|
||||
pub fn parse_drive(path: &str) -> IResult<&str, char> {
|
||||
context("drive", satisfy(char::is_alpha))(path).map(|a: (&str, char)| a)
|
||||
}
|
||||
|
||||
pub fn parse_drive_exact(path: &str) -> IResult<&str, char> {
|
||||
context("drive_exact", pair(parse_drive, complete::char(':')))(path)
|
||||
.map(|(path, (drive, _))| (path, drive))
|
||||
}
|
||||
|
||||
pub fn is_windows_verbatim_sep(c: char) -> bool {
|
||||
c == '\\'
|
||||
}
|
||||
|
||||
pub fn is_windows_sep(c: char) -> bool {
|
||||
c == '\\' || c == '/'
|
||||
}
|
||||
|
||||
pub fn parse_windows_next_component(path: &str, verbatim: bool) -> (&str, &str, &str) {
|
||||
let separator = if verbatim {
|
||||
is_windows_verbatim_sep
|
||||
} else {
|
||||
is_windows_sep
|
||||
};
|
||||
let p = path.as_bytes();
|
||||
match p.position(|x| separator(x as char)) {
|
||||
Some(separator_start) => {
|
||||
let separator_end = separator_start + 1;
|
||||
let component = &path[0..separator_start];
|
||||
let path_with_sep = &path[separator_start..];
|
||||
let path_without_sep = &path[separator_end..];
|
||||
|
||||
(component, path_with_sep, path_without_sep)
|
||||
}
|
||||
None => (path, "", ""),
|
||||
}
|
||||
}
|
||||
|
||||
fn context_verify_error<'a>(input: &'a str, context: &'static str) -> nom::Err<Error<&'a str>> {
|
||||
nom::Err::Error(Error::add_context(
|
||||
input,
|
||||
context,
|
||||
Error::from_error_kind(input, ErrorKind::Verify),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn parse_windows_path_prefix(raw_path: &str) -> IResult<&str, Prefix<'_>> {
|
||||
if let Ok((path, _)) = tag(r"\\")(raw_path) as IResult<&str, &str> {
|
||||
if let Ok((path, _)) = tag(r"?\")(path) as IResult<&str, &str> {
|
||||
if let Ok((path, _)) = peek(non_slash)(path) as IResult<&str, &str> {
|
||||
if let Ok((path, _)) = tag(r"UNC\")(path) as IResult<&str, &str> {
|
||||
let (server, _, other) = parse_windows_next_component(path, true);
|
||||
let (share, next_input, _) = parse_windows_next_component(other, true);
|
||||
|
||||
return Ok((next_input, Prefix::VerbatimUNC { server, share }));
|
||||
} else if let Ok((path, drive)) = parse_drive_exact(path) {
|
||||
return Ok((path, Prefix::VerbatimDisk { drive }));
|
||||
} else {
|
||||
let (prefix, next_input, _) = parse_windows_next_component(path, true);
|
||||
return Ok((next_input, Prefix::Verbatim { prefix }));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok((path, _)) = tag(r".\")(path) as IResult<&str, &str> {
|
||||
let (prefix, next_input, _) = parse_windows_next_component(path, false);
|
||||
return Ok((next_input, Prefix::DeviceNS { device: prefix }));
|
||||
}
|
||||
let (server, _, other) = parse_windows_next_component(path, false);
|
||||
let (share, next_input, _) = parse_windows_next_component(other, false);
|
||||
|
||||
if !server.is_empty() && !share.is_empty() {
|
||||
return Ok((next_input, Prefix::UNC { server, share }));
|
||||
}
|
||||
Err(context_verify_error(raw_path, "windows path prefix"))
|
||||
} else if let Ok((path, drive)) = parse_drive_exact(raw_path) {
|
||||
Ok((path, Prefix::Disk { drive }))
|
||||
} else {
|
||||
Err(context_verify_error(raw_path, "windows path prefix"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_parse_windows_path_prefix() {
|
||||
use super::*;
|
||||
assert_eq!(
|
||||
parse_windows_path_prefix(r"\\?\UNC\server\share\path"),
|
||||
Ok((
|
||||
r"\path",
|
||||
Prefix::VerbatimUNC {
|
||||
server: "server",
|
||||
share: "share"
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_windows_path_prefix(r"\\?\C:\path"),
|
||||
Ok((r"\path", Prefix::VerbatimDisk { drive: 'C' }))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_windows_path_prefix(r"\\server\share\path"),
|
||||
Ok((
|
||||
r"\path",
|
||||
Prefix::UNC {
|
||||
server: "server",
|
||||
share: "share"
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_windows_path_prefix(r"C:\path"),
|
||||
Ok((r"\path", Prefix::Disk { drive: 'C' }))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_windows_path_prefix(r"\\.\device\path"),
|
||||
Ok((r"\path", Prefix::DeviceNS { device: "device" }))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_windows_path_prefix(r"\\?\abc\path"),
|
||||
Ok((r"\path", Prefix::Verbatim { prefix: "abc" }))
|
||||
)
|
||||
}
|
||||
}
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@ -2907,7 +2907,7 @@ packages:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
|
||||
'@opentelemetry/instrumentation-lru-memoizer@0.43.0':
|
||||
resolution: {integrity: sha512-fZc+1eJUV+tFxaB3zkbupiA8SL3vhDUq89HbDNg1asweYrEb9OlHIB+Ot14ZiHUc1qCmmWmZHbPTwa56mVVwzg==}
|
||||
resolution: {integrity: sha512-fZc+1eJUV+tFxaB3zkbupiA8SL3vhDUq89HbDNg1aswcolor_eyre::eyreb9OlHIB+Ot14ZiHUc1qCmmWmZHbPTwa56mVVwzg==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.3.0
|
||||
|
Loading…
Reference in New Issue
Block a user