fix: fix mikan rss extractors
This commit is contained in:
parent
5bc5d98823
commit
f327ea29f1
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -5055,6 +5055,7 @@ dependencies = [
|
|||||||
"sea-orm",
|
"sea-orm",
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
"seaography",
|
"seaography",
|
||||||
|
"secrecy",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
@ -5908,6 +5909,15 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "secrecy"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
|
@ -113,6 +113,7 @@ http-cache = { version = "0.20.0", features = [
|
|||||||
http-cache-semantics = "2.1.0"
|
http-cache-semantics = "2.1.0"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
nom = "8.0.0"
|
nom = "8.0.0"
|
||||||
|
secrecy = "0.10.3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = "3"
|
serial_test = "3"
|
||||||
|
@ -9,7 +9,6 @@ use loco_rs::{
|
|||||||
};
|
};
|
||||||
use recorder::{
|
use recorder::{
|
||||||
app::App,
|
app::App,
|
||||||
extract::mikan::parse_mikan_rss_items_from_rss_link,
|
|
||||||
migrations::Migrator,
|
migrations::Migrator,
|
||||||
models::{
|
models::{
|
||||||
subscribers::SEED_SUBSCRIBER,
|
subscribers::SEED_SUBSCRIBER,
|
||||||
|
@ -4,21 +4,25 @@ use thiserror::Error;
|
|||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ExtractError {
|
pub enum ExtractError {
|
||||||
#[error("Parse bangumi season error: {0}")]
|
#[error("Extract bangumi season error: {0}")]
|
||||||
BangumiSeasonError(#[from] std::num::ParseIntError),
|
BangumiSeasonError(#[from] std::num::ParseIntError),
|
||||||
#[error("Parse file url error: {0}")]
|
#[error("Extract file url error: {0}")]
|
||||||
FileUrlError(#[from] url::ParseError),
|
FileUrlError(#[from] url::ParseError),
|
||||||
#[error("Parse {desc} with mime error, expected {expected}, but got {found}")]
|
#[error("Extract {desc} with mime error, expected {expected}, but got {found}")]
|
||||||
MimeError {
|
MimeError {
|
||||||
desc: String,
|
desc: String,
|
||||||
expected: String,
|
expected: String,
|
||||||
found: String,
|
found: String,
|
||||||
},
|
},
|
||||||
#[error("Parse mikan rss {url} format error")]
|
#[error("Invalid or unknown format in extracting mikan rss")]
|
||||||
MikanRssFormatError { url: String },
|
MikanRssInvalidFormatError,
|
||||||
#[error("Parse mikan rss item format error, {reason}")]
|
#[error("Invalid field {field} in extracting mikan rss")]
|
||||||
MikanRssItemFormatError { reason: String },
|
MikanRssInvalidFieldError {
|
||||||
#[error("Missing field {field} in extracting meta")]
|
field: Cow<'static, str>,
|
||||||
|
#[source]
|
||||||
|
source: Option<Box<dyn StdError + Send + Sync>>,
|
||||||
|
},
|
||||||
|
#[error("Missing field {field} in extracting mikan meta")]
|
||||||
MikanMetaMissingFieldError {
|
MikanMetaMissingFieldError {
|
||||||
field: Cow<'static, str>,
|
field: Cow<'static, str>,
|
||||||
#[source]
|
#[source]
|
||||||
@ -33,4 +37,21 @@ impl ExtractError {
|
|||||||
source: None,
|
source: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_mikan_rss_invalid_field(field: Cow<'static, str>) -> Self {
|
||||||
|
Self::MikanRssInvalidFieldError {
|
||||||
|
field,
|
||||||
|
source: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_mikan_rss_invalid_field_and_source(
|
||||||
|
field: Cow<'static, str>,
|
||||||
|
source: Box<dyn StdError + Send + Sync>,
|
||||||
|
) -> Self {
|
||||||
|
Self::MikanRssInvalidFieldError {
|
||||||
|
field,
|
||||||
|
source: Some(source),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,14 @@ use std::ops::Deref;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use loco_rs::app::{AppContext, Initializer};
|
use loco_rs::app::{AppContext, Initializer};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::AppMikanConfig;
|
use super::AppMikanConfig;
|
||||||
use crate::{config::AppConfigExt, fetch::HttpClient};
|
use crate::{
|
||||||
|
config::AppConfigExt,
|
||||||
|
fetch::{HttpClient, HttpClientTrait},
|
||||||
|
};
|
||||||
|
|
||||||
static APP_MIKAN_CLIENT: OnceCell<AppMikanClient> = OnceCell::new();
|
static APP_MIKAN_CLIENT: OnceCell<AppMikanClient> = OnceCell::new();
|
||||||
|
|
||||||
@ -39,13 +43,15 @@ impl AppMikanClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for AppMikanClient {
|
impl Deref for AppMikanClient {
|
||||||
type Target = HttpClient;
|
type Target = ClientWithMiddleware;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.http_client
|
self.http_client.deref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HttpClientTrait for AppMikanClient {}
|
||||||
|
|
||||||
pub struct AppMikanClientInitializer;
|
pub struct AppMikanClientInitializer;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -12,8 +12,7 @@ pub use rss_extract::{
|
|||||||
MikanRssChannel, MikanRssItem, MikanSubscriberAggregationRssChannel,
|
MikanRssChannel, MikanRssItem, MikanSubscriberAggregationRssChannel,
|
||||||
MikanSubscriberAggregationRssLink, build_mikan_bangumi_rss_link,
|
MikanSubscriberAggregationRssLink, build_mikan_bangumi_rss_link,
|
||||||
build_mikan_subscriber_aggregation_rss_link, extract_mikan_bangumi_id_from_rss_link,
|
build_mikan_subscriber_aggregation_rss_link, extract_mikan_bangumi_id_from_rss_link,
|
||||||
extract_mikan_subscriber_aggregation_id_from_rss_link, parse_mikan_rss_channel_from_rss_link,
|
extract_mikan_rss_channel_from_rss_link, extract_mikan_subscriber_aggregation_id_from_rss_link,
|
||||||
parse_mikan_rss_items_from_rss_link,
|
|
||||||
};
|
};
|
||||||
pub use web_extract::{
|
pub use web_extract::{
|
||||||
MikanBangumiMeta, MikanEpisodeMeta, build_mikan_bangumi_homepage, build_mikan_episode_homepage,
|
MikanBangumiMeta, MikanEpisodeMeta, build_mikan_bangumi_homepage, build_mikan_episode_homepage,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use std::ops::Deref;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use color_eyre::eyre;
|
use color_eyre::eyre;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use reqwest::IntoUrl;
|
use reqwest::IntoUrl;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::instrument;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -105,50 +106,55 @@ impl TryFrom<rss::Item> for MikanRssItem {
|
|||||||
type Error = ExtractError;
|
type Error = ExtractError;
|
||||||
|
|
||||||
fn try_from(item: rss::Item) -> Result<Self, Self::Error> {
|
fn try_from(item: rss::Item) -> Result<Self, Self::Error> {
|
||||||
let mime_type = item
|
let enclosure = item.enclosure.ok_or_else(|| {
|
||||||
.enclosure()
|
ExtractError::from_mikan_rss_invalid_field(Cow::Borrowed("enclosure"))
|
||||||
.map(|x| x.mime_type.to_string())
|
})?;
|
||||||
.unwrap_or_default();
|
|
||||||
if mime_type == BITTORRENT_MIME_TYPE {
|
let mime_type = enclosure.mime_type;
|
||||||
let enclosure = item.enclosure.unwrap();
|
if mime_type != BITTORRENT_MIME_TYPE {
|
||||||
|
return Err(ExtractError::MimeError {
|
||||||
|
expected: String::from(BITTORRENT_MIME_TYPE),
|
||||||
|
found: mime_type.to_string(),
|
||||||
|
desc: String::from("MikanRssItem"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = item.title.ok_or_else(|| {
|
||||||
|
ExtractError::from_mikan_rss_invalid_field(Cow::Borrowed("title:title"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let enclosure_url = Url::parse(&enclosure.url).map_err(|inner| {
|
||||||
|
ExtractError::from_mikan_rss_invalid_field_and_source(
|
||||||
|
Cow::Borrowed("enclosure_url:enclosure.link"),
|
||||||
|
Box::new(inner),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let homepage = item
|
let homepage = item
|
||||||
.link
|
.link
|
||||||
.ok_or_else(|| ExtractError::MikanRssItemFormatError {
|
.and_then(|link| Url::parse(&link).ok())
|
||||||
reason: String::from("must to have link for homepage"),
|
.ok_or_else(|| {
|
||||||
|
ExtractError::from_mikan_rss_invalid_field(Cow::Borrowed("homepage:link"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let homepage = Url::parse(&homepage)?;
|
|
||||||
|
|
||||||
let enclosure_url = Url::parse(&enclosure.url)?;
|
|
||||||
|
|
||||||
let MikanEpisodeHomepage {
|
let MikanEpisodeHomepage {
|
||||||
mikan_episode_id, ..
|
mikan_episode_id, ..
|
||||||
} = parse_mikan_episode_id_from_homepage(&homepage).ok_or_else(|| {
|
} = parse_mikan_episode_id_from_homepage(&homepage).ok_or_else(|| {
|
||||||
ExtractError::MikanRssItemFormatError {
|
ExtractError::from_mikan_rss_invalid_field(Cow::Borrowed("mikan_episode_id"))
|
||||||
reason: String::from("homepage link format invalid"),
|
|
||||||
}
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(MikanRssItem {
|
Ok(MikanRssItem {
|
||||||
title: item.title.unwrap_or_default(),
|
title,
|
||||||
homepage,
|
homepage,
|
||||||
url: enclosure_url,
|
url: enclosure_url,
|
||||||
content_length: enclosure.length.parse().ok(),
|
content_length: enclosure.length.parse().ok(),
|
||||||
mime: enclosure.mime_type,
|
mime: mime_type,
|
||||||
pub_date: item
|
pub_date: item
|
||||||
.pub_date
|
.pub_date
|
||||||
.and_then(|s| DateTime::parse_from_rfc2822(&s).ok())
|
.and_then(|s| DateTime::parse_from_rfc2822(&s).ok())
|
||||||
.map(|s| s.timestamp_millis()),
|
.map(|s| s.timestamp_millis()),
|
||||||
mikan_episode_id,
|
mikan_episode_id,
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
Err(ExtractError::MimeError {
|
|
||||||
expected: String::from(BITTORRENT_MIME_TYPE),
|
|
||||||
found: mime_type,
|
|
||||||
desc: String::from("MikanRssItem"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,21 +226,12 @@ pub fn extract_mikan_subscriber_aggregation_id_from_rss_link(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn parse_mikan_rss_items_from_rss_link(
|
#[instrument(skip_all, fields(channel_rss_link = channel_rss_link.as_str()))]
|
||||||
client: Option<&AppMikanClient>,
|
pub async fn extract_mikan_rss_channel_from_rss_link(
|
||||||
url: impl IntoUrl,
|
http_client: &AppMikanClient,
|
||||||
) -> eyre::Result<Vec<MikanRssItem>> {
|
channel_rss_link: impl IntoUrl,
|
||||||
let channel = parse_mikan_rss_channel_from_rss_link(client, url).await?;
|
|
||||||
|
|
||||||
Ok(channel.into_items())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn parse_mikan_rss_channel_from_rss_link(
|
|
||||||
client: Option<&AppMikanClient>,
|
|
||||||
url: impl IntoUrl,
|
|
||||||
) -> eyre::Result<MikanRssChannel> {
|
) -> eyre::Result<MikanRssChannel> {
|
||||||
let http_client = client.map(|s| s.deref());
|
let bytes = fetch_bytes(http_client, channel_rss_link.as_str()).await?;
|
||||||
let bytes = fetch_bytes(http_client, url.as_str()).await?;
|
|
||||||
|
|
||||||
let channel = rss::Channel::read_from(&bytes[..])?;
|
let channel = rss::Channel::read_from(&bytes[..])?;
|
||||||
|
|
||||||
@ -245,16 +242,34 @@ pub async fn parse_mikan_rss_channel_from_rss_link(
|
|||||||
mikan_fansub_id,
|
mikan_fansub_id,
|
||||||
}) = extract_mikan_bangumi_id_from_rss_link(&channel_link)
|
}) = extract_mikan_bangumi_id_from_rss_link(&channel_link)
|
||||||
{
|
{
|
||||||
|
tracing::trace!(
|
||||||
|
mikan_bangumi_id,
|
||||||
|
mikan_fansub_id,
|
||||||
|
"MikanBangumiRssLink extracting..."
|
||||||
|
);
|
||||||
|
|
||||||
let channel_name = channel.title().replace("Mikan Project - ", "");
|
let channel_name = channel.title().replace("Mikan Project - ", "");
|
||||||
|
|
||||||
let items = channel
|
let items = channel
|
||||||
.items
|
.items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// @TODO log error
|
.enumerate()
|
||||||
.flat_map(MikanRssItem::try_from)
|
.flat_map(|(idx, item)| {
|
||||||
|
MikanRssItem::try_from(item).inspect_err(
|
||||||
|
|error| tracing::warn!(error = %error, "failed to extract rss item idx = {}", idx),
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
|
|
||||||
if let Some(mikan_fansub_id) = mikan_fansub_id {
|
if let Some(mikan_fansub_id) = mikan_fansub_id {
|
||||||
|
tracing::trace!(
|
||||||
|
channel_name,
|
||||||
|
channel_link = channel_link.as_str(),
|
||||||
|
mikan_bangumi_id,
|
||||||
|
mikan_fansub_id,
|
||||||
|
"MikanBangumiRssChannel extracted"
|
||||||
|
);
|
||||||
|
|
||||||
Ok(MikanRssChannel::Bangumi(MikanBangumiRssChannel {
|
Ok(MikanRssChannel::Bangumi(MikanBangumiRssChannel {
|
||||||
name: channel_name,
|
name: channel_name,
|
||||||
mikan_bangumi_id,
|
mikan_bangumi_id,
|
||||||
@ -263,6 +278,13 @@ pub async fn parse_mikan_rss_channel_from_rss_link(
|
|||||||
items,
|
items,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
|
tracing::trace!(
|
||||||
|
channel_name,
|
||||||
|
channel_link = channel_link.as_str(),
|
||||||
|
mikan_bangumi_id,
|
||||||
|
"MikanBangumiAggregationRssChannel extracted"
|
||||||
|
);
|
||||||
|
|
||||||
Ok(MikanRssChannel::BangumiAggregation(
|
Ok(MikanRssChannel::BangumiAggregation(
|
||||||
MikanBangumiAggregationRssChannel {
|
MikanBangumiAggregationRssChannel {
|
||||||
name: channel_name,
|
name: channel_name,
|
||||||
@ -277,25 +299,41 @@ pub async fn parse_mikan_rss_channel_from_rss_link(
|
|||||||
..
|
..
|
||||||
}) = extract_mikan_subscriber_aggregation_id_from_rss_link(&channel_link)
|
}) = extract_mikan_subscriber_aggregation_id_from_rss_link(&channel_link)
|
||||||
{
|
{
|
||||||
|
tracing::trace!(
|
||||||
|
mikan_aggregation_id,
|
||||||
|
"MikanSubscriberAggregationRssLink extracting..."
|
||||||
|
);
|
||||||
|
|
||||||
let items = channel
|
let items = channel
|
||||||
.items
|
.items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// @TODO log error
|
.enumerate()
|
||||||
.flat_map(MikanRssItem::try_from)
|
.flat_map(|(idx, item)| {
|
||||||
|
MikanRssItem::try_from(item).inspect_err(
|
||||||
|
|error| tracing::warn!(error = %error, "failed to extract rss item idx = {}", idx),
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
|
|
||||||
return Ok(MikanRssChannel::SubscriberAggregation(
|
tracing::trace!(
|
||||||
|
channel_link = channel_link.as_str(),
|
||||||
|
mikan_aggregation_id,
|
||||||
|
"MikanSubscriberAggregationRssChannel extracted"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(MikanRssChannel::SubscriberAggregation(
|
||||||
MikanSubscriberAggregationRssChannel {
|
MikanSubscriberAggregationRssChannel {
|
||||||
mikan_aggregation_id,
|
mikan_aggregation_id,
|
||||||
items,
|
items,
|
||||||
url: channel_link,
|
url: channel_link,
|
||||||
},
|
},
|
||||||
));
|
))
|
||||||
} else {
|
} else {
|
||||||
return Err(ExtractError::MikanRssFormatError {
|
Err(ExtractError::MikanRssInvalidFormatError)
|
||||||
url: url.as_str().into(),
|
.inspect_err(|error| {
|
||||||
}
|
tracing::warn!(error = %error);
|
||||||
.into());
|
})
|
||||||
|
.map_err(|error| error.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,20 +341,39 @@ pub async fn parse_mikan_rss_channel_from_rss_link(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::assert_matches::assert_matches;
|
use std::assert_matches::assert_matches;
|
||||||
|
|
||||||
|
use color_eyre::eyre;
|
||||||
|
use rstest::rstest;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
extract::mikan::{
|
extract::mikan::{
|
||||||
MikanBangumiAggregationRssChannel, MikanBangumiRssChannel, MikanRssChannel,
|
MikanBangumiAggregationRssChannel, MikanBangumiRssChannel, MikanRssChannel,
|
||||||
parse_mikan_rss_channel_from_rss_link,
|
extract_mikan_rss_channel_from_rss_link,
|
||||||
},
|
},
|
||||||
sync::core::BITTORRENT_MIME_TYPE,
|
sync::core::BITTORRENT_MIME_TYPE,
|
||||||
|
test_utils::mikan::build_testing_mikan_client,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
pub async fn test_parse_mikan_rss_channel_from_rss_link() {
|
async fn test_parse_mikan_rss_channel_from_rss_link() -> eyre::Result<()> {
|
||||||
{
|
let mut mikan_server = mockito::Server::new_async().await;
|
||||||
let bangumi_url = "https://mikanani.me/RSS/Bangumi?bangumiId=3141&subgroupid=370";
|
|
||||||
|
|
||||||
let channel = parse_mikan_rss_channel_from_rss_link(None, bangumi_url)
|
let mikan_base_url = Url::parse(&mikan_server.url())?;
|
||||||
|
|
||||||
|
let mikan_client = build_testing_mikan_client(mikan_base_url.clone())?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let bangumi_rss_url =
|
||||||
|
mikan_base_url.join("/RSS/Bangumi?bangumiId=3141&subgroupid=370")?;
|
||||||
|
let bangumi_rss_mock = mikan_server
|
||||||
|
.mock("GET", bangumi_rss_url.path())
|
||||||
|
.with_body_from_file("tests/resources/mikan/Bangumi-3141-370.rss")
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let channel = extract_mikan_rss_channel_from_rss_link(&mikan_client, bangumi_rss_url)
|
||||||
.await
|
.await
|
||||||
.expect("should get mikan channel from rss url");
|
.expect("should get mikan channel from rss url");
|
||||||
|
|
||||||
@ -343,11 +400,20 @@ mod tests {
|
|||||||
|
|
||||||
let name = first_sub_item.title.as_str();
|
let name = first_sub_item.title.as_str();
|
||||||
assert!(name.contains("葬送的芙莉莲"));
|
assert!(name.contains("葬送的芙莉莲"));
|
||||||
|
|
||||||
|
bangumi_rss_mock.expect(1);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let bangumi_url = "https://mikanani.me/RSS/Bangumi?bangumiId=3416";
|
let bangumi_rss_url = mikan_base_url.join("/RSS/Bangumi?bangumiId=3416")?;
|
||||||
|
|
||||||
let channel = parse_mikan_rss_channel_from_rss_link(None, bangumi_url)
|
let bangumi_rss_mock = mikan_server
|
||||||
|
.mock("GET", bangumi_rss_url.path())
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.with_body_from_file("tests/resources/mikan/Bangumi-3416.rss")
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let channel = extract_mikan_rss_channel_from_rss_link(&mikan_client, bangumi_rss_url)
|
||||||
.await
|
.await
|
||||||
.expect("should get mikan channel from rss url");
|
.expect("should get mikan channel from rss url");
|
||||||
|
|
||||||
@ -357,6 +423,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_matches!(&channel.name(), Some("叹气的亡灵想隐退"));
|
assert_matches!(&channel.name(), Some("叹气的亡灵想隐退"));
|
||||||
|
|
||||||
|
bangumi_rss_mock.expect(1);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{borrow::Cow, ops::Deref};
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use color_eyre::eyre;
|
use color_eyre::eyre;
|
||||||
@ -117,10 +117,9 @@ pub fn parse_mikan_episode_id_from_homepage(url: &Url) -> Option<MikanEpisodeHom
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn extract_mikan_poster_meta_from_src(
|
pub async fn extract_mikan_poster_meta_from_src(
|
||||||
client: Option<&AppMikanClient>,
|
http_client: &AppMikanClient,
|
||||||
origin_poster_src_url: Url,
|
origin_poster_src_url: Url,
|
||||||
) -> eyre::Result<MikanBangumiPosterMeta> {
|
) -> eyre::Result<MikanBangumiPosterMeta> {
|
||||||
let http_client = client.map(|s| s.deref());
|
|
||||||
let poster_data = fetch_image(http_client, origin_poster_src_url.clone()).await?;
|
let poster_data = fetch_image(http_client, origin_poster_src_url.clone()).await?;
|
||||||
Ok(MikanBangumiPosterMeta {
|
Ok(MikanBangumiPosterMeta {
|
||||||
origin_poster_src: origin_poster_src_url,
|
origin_poster_src: origin_poster_src_url,
|
||||||
@ -152,8 +151,7 @@ pub async fn extract_mikan_bangumi_poster_meta_from_src_with_cache(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let poster_data =
|
let poster_data = fetch_image(mikan_client, origin_poster_src_url.clone()).await?;
|
||||||
fetch_image(Some(mikan_client.deref()), origin_poster_src_url.clone()).await?;
|
|
||||||
|
|
||||||
let poster_str = dal_client
|
let poster_str = dal_client
|
||||||
.store_object(
|
.store_object(
|
||||||
@ -174,10 +172,9 @@ pub async fn extract_mikan_bangumi_poster_meta_from_src_with_cache(
|
|||||||
|
|
||||||
#[instrument(skip_all, fields(mikan_episode_homepage_url = mikan_episode_homepage_url.as_str()))]
|
#[instrument(skip_all, fields(mikan_episode_homepage_url = mikan_episode_homepage_url.as_str()))]
|
||||||
pub async fn extract_mikan_episode_meta_from_episode_homepage(
|
pub async fn extract_mikan_episode_meta_from_episode_homepage(
|
||||||
client: Option<&AppMikanClient>,
|
http_client: &AppMikanClient,
|
||||||
mikan_episode_homepage_url: Url,
|
mikan_episode_homepage_url: Url,
|
||||||
) -> eyre::Result<MikanEpisodeMeta> {
|
) -> eyre::Result<MikanEpisodeMeta> {
|
||||||
let http_client = client.map(|s| s.deref());
|
|
||||||
let mikan_base_url = Url::parse(&mikan_episode_homepage_url.origin().unicode_serialization())?;
|
let mikan_base_url = Url::parse(&mikan_episode_homepage_url.origin().unicode_serialization())?;
|
||||||
let content = fetch_html(http_client, mikan_episode_homepage_url.as_str()).await?;
|
let content = fetch_html(http_client, mikan_episode_homepage_url.as_str()).await?;
|
||||||
|
|
||||||
@ -286,10 +283,9 @@ pub async fn extract_mikan_episode_meta_from_episode_homepage(
|
|||||||
|
|
||||||
#[instrument(skip_all, fields(mikan_bangumi_homepage_url = mikan_bangumi_homepage_url.as_str()))]
|
#[instrument(skip_all, fields(mikan_bangumi_homepage_url = mikan_bangumi_homepage_url.as_str()))]
|
||||||
pub async fn extract_mikan_bangumi_meta_from_bangumi_homepage(
|
pub async fn extract_mikan_bangumi_meta_from_bangumi_homepage(
|
||||||
client: Option<&AppMikanClient>,
|
http_client: &AppMikanClient,
|
||||||
mikan_bangumi_homepage_url: Url,
|
mikan_bangumi_homepage_url: Url,
|
||||||
) -> eyre::Result<MikanBangumiMeta> {
|
) -> eyre::Result<MikanBangumiMeta> {
|
||||||
let http_client = client.map(|s| s.deref());
|
|
||||||
let mikan_base_url = Url::parse(&mikan_bangumi_homepage_url.origin().unicode_serialization())?;
|
let mikan_base_url = Url::parse(&mikan_bangumi_homepage_url.origin().unicode_serialization())?;
|
||||||
let content = fetch_html(http_client, mikan_bangumi_homepage_url.as_str()).await?;
|
let content = fetch_html(http_client, mikan_bangumi_homepage_url.as_str()).await?;
|
||||||
let html = Html::parse_document(&content);
|
let html = Html::parse_document(&content);
|
||||||
@ -369,10 +365,9 @@ pub async fn extract_mikan_bangumi_meta_from_bangumi_homepage(
|
|||||||
*/
|
*/
|
||||||
#[instrument(skip_all, fields(my_bangumi_page_url = my_bangumi_page_url.as_str()))]
|
#[instrument(skip_all, fields(my_bangumi_page_url = my_bangumi_page_url.as_str()))]
|
||||||
pub async fn extract_mikan_bangumis_meta_from_my_bangumi_page(
|
pub async fn extract_mikan_bangumis_meta_from_my_bangumi_page(
|
||||||
client: Option<&AppMikanClient>,
|
http_client: &AppMikanClient,
|
||||||
my_bangumi_page_url: Url,
|
my_bangumi_page_url: Url,
|
||||||
) -> eyre::Result<Vec<MikanBangumiMeta>> {
|
) -> eyre::Result<Vec<MikanBangumiMeta>> {
|
||||||
let http_client = client.map(|c| c.deref());
|
|
||||||
let mikan_base_url = Url::parse(&my_bangumi_page_url.origin().unicode_serialization())?;
|
let mikan_base_url = Url::parse(&my_bangumi_page_url.origin().unicode_serialization())?;
|
||||||
|
|
||||||
let content = fetch_html(http_client, my_bangumi_page_url.clone()).await?;
|
let content = fetch_html(http_client, my_bangumi_page_url.clone()).await?;
|
||||||
@ -506,7 +501,7 @@ mod test {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let bgm_poster =
|
let bgm_poster =
|
||||||
extract_mikan_poster_meta_from_src(Some(&mikan_client), bangumi_poster_url).await?;
|
extract_mikan_poster_meta_from_src(&mikan_client, bangumi_poster_url).await?;
|
||||||
bangumi_poster_mock.expect(1);
|
bangumi_poster_mock.expect(1);
|
||||||
let u8_data = bgm_poster.poster_data.expect("should have poster data");
|
let u8_data = bgm_poster.poster_data.expect("should have poster data");
|
||||||
let image = Image::read(u8_data.to_vec(), Default::default());
|
let image = Image::read(u8_data.to_vec(), Default::default());
|
||||||
@ -540,7 +535,7 @@ mod test {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let ep_meta = extract_mikan_episode_meta_from_episode_homepage(
|
let ep_meta = extract_mikan_episode_meta_from_episode_homepage(
|
||||||
Some(&mikan_client),
|
&mikan_client,
|
||||||
episode_homepage_url.clone(),
|
episode_homepage_url.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -579,7 +574,7 @@ mod test {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let bgm_meta = extract_mikan_bangumi_meta_from_bangumi_homepage(
|
let bgm_meta = extract_mikan_bangumi_meta_from_bangumi_homepage(
|
||||||
Some(&mikan_client),
|
&mikan_client,
|
||||||
bangumi_homepage_url.clone(),
|
bangumi_homepage_url.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -613,31 +608,29 @@ mod test {
|
|||||||
|
|
||||||
let my_bangumi_page_url = mikan_base_url.join("/Home/MyBangumi")?;
|
let my_bangumi_page_url = mikan_base_url.join("/Home/MyBangumi")?;
|
||||||
|
|
||||||
let mock_my_bangumi = mikan_server
|
let my_bangumi_mock = mikan_server
|
||||||
.mock("GET", my_bangumi_page_url.path())
|
.mock("GET", my_bangumi_page_url.path())
|
||||||
.with_body_from_file("tests/resources/mikan/MyBangumi.htm")
|
.with_body_from_file("tests/resources/mikan/MyBangumi.htm")
|
||||||
.create_async()
|
.create_async()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let mock_expand_bangumi = mikan_server
|
let expand_bangumi_mock = mikan_server
|
||||||
.mock("GET", "/ExpandBangumi")
|
.mock("GET", "/ExpandBangumi")
|
||||||
.match_query(mockito::Matcher::Any)
|
.match_query(mockito::Matcher::Any)
|
||||||
.with_body_from_file("tests/resources/mikan/ExpandBangumi.htm")
|
.with_body_from_file("tests/resources/mikan/ExpandBangumi.htm")
|
||||||
.create_async()
|
.create_async()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let bangumi_metas = extract_mikan_bangumis_meta_from_my_bangumi_page(
|
let bangumi_metas =
|
||||||
Some(&mikan_client),
|
extract_mikan_bangumis_meta_from_my_bangumi_page(&mikan_client, my_bangumi_page_url)
|
||||||
my_bangumi_page_url,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
assert!(!bangumi_metas.is_empty());
|
assert!(!bangumi_metas.is_empty());
|
||||||
|
|
||||||
assert!(bangumi_metas[0].origin_poster_src.is_some());
|
assert!(bangumi_metas[0].origin_poster_src.is_some());
|
||||||
|
|
||||||
mock_my_bangumi.expect(1);
|
my_bangumi_mock.expect(1);
|
||||||
mock_expand_bangumi.expect(bangumi_metas.len());
|
expand_bangumi_mock.expect(bangumi_metas.len());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use reqwest::IntoUrl;
|
use reqwest::IntoUrl;
|
||||||
|
|
||||||
use super::HttpClient;
|
use super::client::HttpClientTrait;
|
||||||
|
|
||||||
pub async fn fetch_bytes<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> color_eyre::eyre::Result<Bytes> {
|
|
||||||
let client = client.unwrap_or_default();
|
|
||||||
|
|
||||||
|
pub async fn fetch_bytes<T: IntoUrl, H: HttpClientTrait>(
|
||||||
|
client: &H,
|
||||||
|
url: T,
|
||||||
|
) -> color_eyre::eyre::Result<Bytes> {
|
||||||
let bytes = client
|
let bytes = client
|
||||||
.get(url)
|
.get(url)
|
||||||
.send()
|
.send()
|
||||||
|
@ -6,7 +6,6 @@ use http_cache_reqwest::{
|
|||||||
CACacheManager, Cache, CacheManager, CacheMode, HttpCache, HttpCacheOptions, MokaManager,
|
CACacheManager, Cache, CacheManager, CacheMode, HttpCache, HttpCacheOptions, MokaManager,
|
||||||
};
|
};
|
||||||
use leaky_bucket::RateLimiter;
|
use leaky_bucket::RateLimiter;
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use reqwest::{ClientBuilder, Request, Response};
|
use reqwest::{ClientBuilder, Request, Response};
|
||||||
use reqwest_middleware::{
|
use reqwest_middleware::{
|
||||||
ClientBuilder as ClientWithMiddlewareBuilder, ClientWithMiddleware, Next,
|
ClientBuilder as ClientWithMiddlewareBuilder, ClientWithMiddleware, Next,
|
||||||
@ -20,6 +19,23 @@ use thiserror::Error;
|
|||||||
use super::get_random_mobile_ua;
|
use super::get_random_mobile_ua;
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
|
||||||
|
pub struct RateLimiterMiddleware {
|
||||||
|
rate_limiter: RateLimiter,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl reqwest_middleware::Middleware for RateLimiterMiddleware {
|
||||||
|
async fn handle(
|
||||||
|
&self,
|
||||||
|
req: Request,
|
||||||
|
extensions: &'_ mut Extensions,
|
||||||
|
next: Next<'_>,
|
||||||
|
) -> reqwest_middleware::Result<Response> {
|
||||||
|
self.rate_limiter.acquire_one().await;
|
||||||
|
next.run(req, extensions).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
#[serde(rename_all = "snake_case", tag = "type")]
|
||||||
pub enum HttpClientCacheBackendConfig {
|
pub enum HttpClientCacheBackendConfig {
|
||||||
@ -96,6 +112,8 @@ pub enum HttpClientError {
|
|||||||
HttpError(#[from] http::Error),
|
HttpError(#[from] http::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait HttpClientTrait: Deref<Target = ClientWithMiddleware> {}
|
||||||
|
|
||||||
pub struct HttpClient {
|
pub struct HttpClient {
|
||||||
client: ClientWithMiddleware,
|
client: ClientWithMiddleware,
|
||||||
pub config: HttpClientConfig,
|
pub config: HttpClientConfig,
|
||||||
@ -123,23 +141,6 @@ impl Deref for HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RateLimiterMiddleware {
|
|
||||||
rate_limiter: RateLimiter,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl reqwest_middleware::Middleware for RateLimiterMiddleware {
|
|
||||||
async fn handle(
|
|
||||||
&self,
|
|
||||||
req: Request,
|
|
||||||
extensions: &'_ mut Extensions,
|
|
||||||
next: Next<'_>,
|
|
||||||
) -> reqwest_middleware::Result<Response> {
|
|
||||||
self.rate_limiter.acquire_one().await;
|
|
||||||
next.run(req, extensions).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HttpClient {
|
impl HttpClient {
|
||||||
pub fn from_config(config: HttpClientConfig) -> Result<Self, HttpClientError> {
|
pub fn from_config(config: HttpClientConfig) -> Result<Self, HttpClientError> {
|
||||||
let reqwest_client_builder = ClientBuilder::new().user_agent(
|
let reqwest_client_builder = ClientBuilder::new().user_agent(
|
||||||
@ -234,16 +235,10 @@ impl HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static DEFAULT_HTTP_CLIENT: OnceCell<HttpClient> = OnceCell::new();
|
|
||||||
|
|
||||||
impl Default for HttpClient {
|
impl Default for HttpClient {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
HttpClient::from_config(Default::default()).expect("Failed to create default HttpClient")
|
HttpClient::from_config(Default::default()).expect("Failed to create default HttpClient")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for &HttpClient {
|
impl HttpClientTrait for HttpClient {}
|
||||||
fn default() -> Self {
|
|
||||||
DEFAULT_HTTP_CLIENT.get_or_init(HttpClient::default)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
use reqwest::IntoUrl;
|
use reqwest::IntoUrl;
|
||||||
|
|
||||||
use super::HttpClient;
|
use super::client::HttpClientTrait;
|
||||||
|
|
||||||
pub async fn fetch_html<T: IntoUrl>(
|
pub async fn fetch_html<T: IntoUrl, H: HttpClientTrait>(
|
||||||
client: Option<&HttpClient>,
|
client: &H,
|
||||||
url: T,
|
url: T,
|
||||||
) -> color_eyre::eyre::Result<String> {
|
) -> color_eyre::eyre::Result<String> {
|
||||||
let client = client.unwrap_or_default();
|
|
||||||
let content = client
|
let content = client
|
||||||
.get(url)
|
.get(url)
|
||||||
.send()
|
.send()
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use reqwest::IntoUrl;
|
use reqwest::IntoUrl;
|
||||||
|
|
||||||
use super::{bytes::fetch_bytes, HttpClient};
|
use super::{bytes::fetch_bytes, client::HttpClientTrait};
|
||||||
|
|
||||||
pub async fn fetch_image<T: IntoUrl>(client: Option<&HttpClient>, url: T) -> color_eyre::eyre::Result<Bytes> {
|
pub async fn fetch_image<T: IntoUrl, H: HttpClientTrait>(
|
||||||
|
client: &H,
|
||||||
|
url: T,
|
||||||
|
) -> color_eyre::eyre::Result<Bytes> {
|
||||||
fetch_bytes(client, url).await
|
fetch_bytes(client, url).await
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,6 @@ pub mod oidc;
|
|||||||
pub use core::get_random_mobile_ua;
|
pub use core::get_random_mobile_ua;
|
||||||
|
|
||||||
pub use bytes::fetch_bytes;
|
pub use bytes::fetch_bytes;
|
||||||
pub use client::{HttpClient, HttpClientConfig, HttpClientError};
|
pub use client::{HttpClient, HttpClientConfig, HttpClientError, HttpClientTrait};
|
||||||
pub use html::fetch_html;
|
pub use html::fetch_html;
|
||||||
pub use image::fetch_image;
|
pub use image::fetch_image;
|
||||||
|
@ -14,7 +14,7 @@ use crate::{
|
|||||||
build_mikan_bangumi_homepage, build_mikan_bangumi_rss_link,
|
build_mikan_bangumi_homepage, build_mikan_bangumi_rss_link,
|
||||||
extract_mikan_bangumi_meta_from_bangumi_homepage,
|
extract_mikan_bangumi_meta_from_bangumi_homepage,
|
||||||
extract_mikan_episode_meta_from_episode_homepage,
|
extract_mikan_episode_meta_from_episode_homepage,
|
||||||
parse_mikan_rss_channel_from_rss_link,
|
extract_mikan_rss_channel_from_rss_link,
|
||||||
web_extract::{
|
web_extract::{
|
||||||
MikanBangumiPosterMeta, extract_mikan_bangumi_poster_meta_from_src_with_cache,
|
MikanBangumiPosterMeta, extract_mikan_bangumi_poster_meta_from_src_with_cache,
|
||||||
},
|
},
|
||||||
@ -220,8 +220,7 @@ impl Model {
|
|||||||
SubscriptionCategory::Mikan => {
|
SubscriptionCategory::Mikan => {
|
||||||
let mikan_client = ctx.get_mikan_client();
|
let mikan_client = ctx.get_mikan_client();
|
||||||
let channel =
|
let channel =
|
||||||
parse_mikan_rss_channel_from_rss_link(Some(mikan_client), &self.source_url)
|
extract_mikan_rss_channel_from_rss_link(mikan_client, &self.source_url).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let items = channel.into_items();
|
let items = channel.into_items();
|
||||||
|
|
||||||
@ -257,7 +256,7 @@ impl Model {
|
|||||||
for new_rss_item in new_rss_items.iter() {
|
for new_rss_item in new_rss_items.iter() {
|
||||||
new_metas.push(
|
new_metas.push(
|
||||||
extract_mikan_episode_meta_from_episode_homepage(
|
extract_mikan_episode_meta_from_episode_homepage(
|
||||||
Some(mikan_client),
|
mikan_client,
|
||||||
new_rss_item.homepage.clone(),
|
new_rss_item.homepage.clone(),
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
@ -290,7 +289,7 @@ impl Model {
|
|||||||
mikan_fansub_id.to_string(),
|
mikan_fansub_id.to_string(),
|
||||||
async |am| -> color_eyre::eyre::Result<()> {
|
async |am| -> color_eyre::eyre::Result<()> {
|
||||||
let bgm_meta = extract_mikan_bangumi_meta_from_bangumi_homepage(
|
let bgm_meta = extract_mikan_bangumi_meta_from_bangumi_homepage(
|
||||||
Some(mikan_client),
|
mikan_client,
|
||||||
bgm_homepage.clone(),
|
bgm_homepage.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -5,7 +5,7 @@ use itertools::Itertools;
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use librqbit_core::{
|
use librqbit_core::{
|
||||||
magnet::Magnet,
|
magnet::Magnet,
|
||||||
torrent_metainfo::{torrent_from_bytes, TorrentMetaV1Owned},
|
torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes},
|
||||||
};
|
};
|
||||||
use quirks_path::{Path, PathBuf};
|
use quirks_path::{Path, PathBuf};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::{QbitTorrent, QbitTorrentContent, TorrentDownloadError};
|
use super::{QbitTorrent, QbitTorrentContent, TorrentDownloadError};
|
||||||
use crate::fetch::{fetch_bytes, HttpClient};
|
use crate::fetch::{HttpClientTrait, fetch_bytes};
|
||||||
|
|
||||||
pub const BITTORRENT_MIME_TYPE: &str = "application/x-bittorrent";
|
pub const BITTORRENT_MIME_TYPE: &str = "application/x-bittorrent";
|
||||||
pub const MAGNET_SCHEMA: &str = "magnet";
|
pub const MAGNET_SCHEMA: &str = "magnet";
|
||||||
@ -57,7 +57,10 @@ pub enum TorrentSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TorrentSource {
|
impl TorrentSource {
|
||||||
pub async fn parse(client: Option<&HttpClient>, url: &str) -> color_eyre::eyre::Result<Self> {
|
pub async fn parse<H: HttpClientTrait>(
|
||||||
|
client: &H,
|
||||||
|
url: &str,
|
||||||
|
) -> color_eyre::eyre::Result<Self> {
|
||||||
let url = Url::parse(url)?;
|
let url = Url::parse(url)?;
|
||||||
let source = if url.scheme() == MAGNET_SCHEMA {
|
let source = if url.scheme() == MAGNET_SCHEMA {
|
||||||
TorrentSource::from_magnet_url(url)?
|
TorrentSource::from_magnet_url(url)?
|
||||||
|
@ -10,8 +10,8 @@ pub use qbit_rs::model::{
|
|||||||
TorrentFilter as QbitTorrentFilter, TorrentSource as QbitTorrentSource,
|
TorrentFilter as QbitTorrentFilter, TorrentSource as QbitTorrentSource,
|
||||||
};
|
};
|
||||||
use qbit_rs::{
|
use qbit_rs::{
|
||||||
model::{AddTorrentArg, Credential, GetTorrentListArg, NonEmptyStr, SyncData},
|
|
||||||
Qbit,
|
Qbit,
|
||||||
|
model::{AddTorrentArg, Credential, GetTorrentListArg, NonEmptyStr, SyncData},
|
||||||
};
|
};
|
||||||
use quirks_path::{Path, PathBuf};
|
use quirks_path::{Path, PathBuf};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
@ -19,8 +19,8 @@ use tracing::instrument;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
utils::path_equals_as_file_url, Torrent, TorrentDownloadError, TorrentDownloader,
|
Torrent, TorrentDownloadError, TorrentDownloader, TorrentFilter, TorrentSource,
|
||||||
TorrentFilter, TorrentSource,
|
utils::path_equals_as_file_url,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl From<TorrentSource> for QbitTorrentSource {
|
impl From<TorrentSource> for QbitTorrentSource {
|
||||||
@ -490,6 +490,7 @@ pub mod tests {
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::test_utils::fetch::build_testing_http_client;
|
||||||
|
|
||||||
fn get_tmp_qbit_test_folder() -> &'static str {
|
fn get_tmp_qbit_test_folder() -> &'static str {
|
||||||
if cfg!(all(windows, not(feature = "testcontainers"))) {
|
if cfg!(all(windows, not(feature = "testcontainers"))) {
|
||||||
@ -500,16 +501,16 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "testcontainers")]
|
#[cfg(feature = "testcontainers")]
|
||||||
pub async fn create_qbit_testcontainer(
|
pub async fn create_qbit_testcontainer()
|
||||||
) -> color_eyre::eyre::Result<testcontainers::ContainerRequest<testcontainers::GenericImage>>
|
-> color_eyre::eyre::Result<testcontainers::ContainerRequest<testcontainers::GenericImage>>
|
||||||
{
|
{
|
||||||
use testcontainers::{
|
use testcontainers::{
|
||||||
|
GenericImage,
|
||||||
core::{
|
core::{
|
||||||
ContainerPort,
|
ContainerPort,
|
||||||
// ReuseDirective,
|
// ReuseDirective,
|
||||||
WaitFor,
|
WaitFor,
|
||||||
},
|
},
|
||||||
GenericImage,
|
|
||||||
};
|
};
|
||||||
use testcontainers_modules::testcontainers::ImageExt;
|
use testcontainers_modules::testcontainers::ImageExt;
|
||||||
|
|
||||||
@ -590,6 +591,7 @@ pub mod tests {
|
|||||||
username: Option<&str>,
|
username: Option<&str>,
|
||||||
password: Option<&str>,
|
password: Option<&str>,
|
||||||
) -> color_eyre::eyre::Result<()> {
|
) -> color_eyre::eyre::Result<()> {
|
||||||
|
let http_client = build_testing_http_client()?;
|
||||||
let base_save_path = Path::new(get_tmp_qbit_test_folder());
|
let base_save_path = Path::new(get_tmp_qbit_test_folder());
|
||||||
|
|
||||||
let mut downloader = QBittorrentDownloader::from_creation(QBittorrentDownloaderCreation {
|
let mut downloader = QBittorrentDownloader::from_creation(QBittorrentDownloaderCreation {
|
||||||
@ -610,7 +612,7 @@ pub mod tests {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let torrent_source = TorrentSource::parse(
|
let torrent_source = TorrentSource::parse(
|
||||||
None,
|
&http_client,
|
||||||
"https://mikanani.me/Download/20240301/47ee2d69e7f19af783ad896541a07b012676f858.torrent"
|
"https://mikanani.me/Download/20240301/47ee2d69e7f19af783ad896541a07b012676f858.torrent"
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
use loco_rs::prelude::*;
|
||||||
|
use secrecy::SecretString;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CreateMikanRSSFromMyBangumiTask {
|
||||||
|
subscriber_id: i32,
|
||||||
|
task_id: String,
|
||||||
|
cookie: SecretString,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Task for CreateMikanRSSFromMyBangumiTask {
|
||||||
|
fn task(&self) -> TaskInfo {
|
||||||
|
TaskInfo {
|
||||||
|
name: format!(
|
||||||
|
"create-mikan-rss-from-my-bangumi-{}-{}",
|
||||||
|
self.subscriber_id, self.task_id
|
||||||
|
),
|
||||||
|
detail: "create mikan rss from my bangumi page for {} {}".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, _app_context: &AppContext, _vars: &task::Vars) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
|
pub mod create_mikan_bangumi_subscriptions_from_my_bangumi_page;
|
||||||
|
8
apps/recorder/src/test_utils/fetch.rs
Normal file
8
apps/recorder/src/test_utils/fetch.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
use color_eyre::eyre;
|
||||||
|
|
||||||
|
use crate::fetch::HttpClient;
|
||||||
|
|
||||||
|
pub fn build_testing_http_client() -> eyre::Result<HttpClient> {
|
||||||
|
let mikan_client = HttpClient::default();
|
||||||
|
Ok(mikan_client)
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod fetch;
|
||||||
pub mod mikan;
|
pub mod mikan;
|
||||||
#[cfg(feature = "testcontainers")]
|
#[cfg(feature = "testcontainers")]
|
||||||
pub mod testcontainers;
|
pub mod testcontainers;
|
||||||
|
488
apps/recorder/tests/resources/mikan/Bangumi-3141-370.rss
Normal file
488
apps/recorder/tests/resources/mikan/Bangumi-3141-370.rss
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Mikan Project - 葬送的芙莉莲</title>
|
||||||
|
<link>http://mikanani.me/RSS/Bangumi?bangumiId=3141&subgroupid=370</link>
|
||||||
|
<description>Mikan Project - 葬送的芙莉莲</description>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren [01-28
|
||||||
|
修正合集][WebRip 1080p HEVC-10bit AAC][简繁日内封字幕][Fin]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/814afcf5d40bd71bb9bdb863a1db94ac41684d2c</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren [01-28 修正合集][WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][Fin]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren [01-28 修正合集][WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][Fin][17.8GB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/814afcf5d40bd71bb9bdb863a1db94ac41684d2c</link>
|
||||||
|
<contentLength>19112603648</contentLength>
|
||||||
|
<pubDate>2024-06-11T19:04:00</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="19112603648"
|
||||||
|
url="https://mikanani.me/Download/20240611/814afcf5d40bd71bb9bdb863a1db94ac41684d2c.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 28 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕][End]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/ae49d7fc3a508076996f0c438d73b24d7f27855d</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 28 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕][End]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 28 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][End][573.41 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/ae49d7fc3a508076996f0c438d73b24d7f27855d</link>
|
||||||
|
<contentLength>601263936</contentLength>
|
||||||
|
<pubDate>2024-03-29T22:53:07.331</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="601263936"
|
||||||
|
url="https://mikanani.me/Download/20240329/ae49d7fc3a508076996f0c438d73b24d7f27855d.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 27 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/722dd30998c41ee0a5c83c72723ff9a8c0090cde</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 27 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 27 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][494.95 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/722dd30998c41ee0a5c83c72723ff9a8c0090cde</link>
|
||||||
|
<contentLength>518992704</contentLength>
|
||||||
|
<pubDate>2024-03-20T09:19:21.39</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="518992704"
|
||||||
|
url="https://mikanani.me/Download/20240320/722dd30998c41ee0a5c83c72723ff9a8c0090cde.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 26 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/2dabb7faff70ee32b4e7e2551b98cdaddc6a36f5</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 26 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 26 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][672.92 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/2dabb7faff70ee32b4e7e2551b98cdaddc6a36f5</link>
|
||||||
|
<contentLength>705607744</contentLength>
|
||||||
|
<pubDate>2024-03-11T12:52:21.662</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="705607744"
|
||||||
|
url="https://mikanani.me/Download/20240311/2dabb7faff70ee32b4e7e2551b98cdaddc6a36f5.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 25 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/3f7f27bcacdfd1d7ac309de53a235c66738add45</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 25 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 25 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][590.25 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/3f7f27bcacdfd1d7ac309de53a235c66738add45</link>
|
||||||
|
<contentLength>618921984</contentLength>
|
||||||
|
<pubDate>2024-03-04T19:08:09.697</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="618921984"
|
||||||
|
url="https://mikanani.me/Download/20240304/3f7f27bcacdfd1d7ac309de53a235c66738add45.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 24 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/a368623412fcb4618d15d00d49ba2a93586d14b6</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 24 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 24 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][513.49 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/a368623412fcb4618d15d00d49ba2a93586d14b6</link>
|
||||||
|
<contentLength>538433280</contentLength>
|
||||||
|
<pubDate>2024-02-29T07:25:41.619</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="538433280"
|
||||||
|
url="https://mikanani.me/Download/20240229/a368623412fcb4618d15d00d49ba2a93586d14b6.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 23 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/475184dce83ea2b82902592a5ac3343f6d54b36a</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 23 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 23 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][573.95 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/475184dce83ea2b82902592a5ac3343f6d54b36a</link>
|
||||||
|
<contentLength>601830208</contentLength>
|
||||||
|
<pubDate>2024-02-22T19:14:39.759</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="601830208"
|
||||||
|
url="https://mikanani.me/Download/20240222/475184dce83ea2b82902592a5ac3343f6d54b36a.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 22 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/b91dd13268cceda1f81f58d659d4c93163864532</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 22 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 22 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][566.31 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/b91dd13268cceda1f81f58d659d4c93163864532</link>
|
||||||
|
<contentLength>593819072</contentLength>
|
||||||
|
<pubDate>2024-02-16T17:50:48.042</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="593819072"
|
||||||
|
url="https://mikanani.me/Download/20240216/b91dd13268cceda1f81f58d659d4c93163864532.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 21 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/4c265b6765e71ca924ddf4868f2c99a0c2da020d</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 21 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 21 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][630.27 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/4c265b6765e71ca924ddf4868f2c99a0c2da020d</link>
|
||||||
|
<contentLength>660886016</contentLength>
|
||||||
|
<pubDate>2024-02-07T19:36:21.368</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="660886016"
|
||||||
|
url="https://mikanani.me/Download/20240207/4c265b6765e71ca924ddf4868f2c99a0c2da020d.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 20 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/e8ff23bec287c053d8766af4043ff5bba833006d</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 20 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 20 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][616.85 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/e8ff23bec287c053d8766af4043ff5bba833006d</link>
|
||||||
|
<contentLength>646814080</contentLength>
|
||||||
|
<pubDate>2024-02-02T21:57:15.963</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="646814080"
|
||||||
|
url="https://mikanani.me/Download/20240202/e8ff23bec287c053d8766af4043ff5bba833006d.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 19 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/a387331be1e70590d52db4c0197455e9ee450579</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 19 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 19 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][621.01 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/a387331be1e70590d52db4c0197455e9ee450579</link>
|
||||||
|
<contentLength>651176192</contentLength>
|
||||||
|
<pubDate>2024-01-25T21:01:59.604</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="651176192"
|
||||||
|
url="https://mikanani.me/Download/20240125/a387331be1e70590d52db4c0197455e9ee450579.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 18 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/059724511d60173251b378b04709aceff92fffb5</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 18 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 18 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][634.12 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/059724511d60173251b378b04709aceff92fffb5</link>
|
||||||
|
<contentLength>664923008</contentLength>
|
||||||
|
<pubDate>2024-01-18T06:57:43.93</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="664923008"
|
||||||
|
url="https://mikanani.me/Download/20240118/059724511d60173251b378b04709aceff92fffb5.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 17 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/872ab5abd72ea223d2a2e36688cc96f83bb71d42</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 17 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 17 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][639.78 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/872ab5abd72ea223d2a2e36688cc96f83bb71d42</link>
|
||||||
|
<contentLength>670857984</contentLength>
|
||||||
|
<pubDate>2024-01-11T06:57:59.057</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="670857984"
|
||||||
|
url="https://mikanani.me/Download/20240111/872ab5abd72ea223d2a2e36688cc96f83bb71d42.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 16 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/68da11f9aa666160cbbe4fa50643b38014164a5d</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 16 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 16 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][772.11 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/68da11f9aa666160cbbe4fa50643b38014164a5d</link>
|
||||||
|
<contentLength>809616000</contentLength>
|
||||||
|
<pubDate>2024-01-09T00:11:59.933</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="809616000"
|
||||||
|
url="https://mikanani.me/Download/20240109/68da11f9aa666160cbbe4fa50643b38014164a5d.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 15 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/ad301000b04ed7d12bd73272d1e6dff5c2ace953</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 15 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 15 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][625.91 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/ad301000b04ed7d12bd73272d1e6dff5c2ace953</link>
|
||||||
|
<contentLength>656314176</contentLength>
|
||||||
|
<pubDate>2023-12-20T18:44:29.433</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="656314176"
|
||||||
|
url="https://mikanani.me/Download/20231220/ad301000b04ed7d12bd73272d1e6dff5c2ace953.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 14 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/4840448f651f59486af6e82d0c2992a79487cd98</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 14 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 14 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][671.9 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/4840448f651f59486af6e82d0c2992a79487cd98</link>
|
||||||
|
<contentLength>704538240</contentLength>
|
||||||
|
<pubDate>2023-12-14T21:31:46.295</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="704538240"
|
||||||
|
url="https://mikanani.me/Download/20231214/4840448f651f59486af6e82d0c2992a79487cd98.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 13 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/576858ed51361cded0edb377a75491ddf70c7bad</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 13 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 13 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][605.13 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/576858ed51361cded0edb377a75491ddf70c7bad</link>
|
||||||
|
<contentLength>634524800</contentLength>
|
||||||
|
<pubDate>2023-12-06T22:12:16.888</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="634524800"
|
||||||
|
url="https://mikanani.me/Download/20231206/576858ed51361cded0edb377a75491ddf70c7bad.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 12 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/fcc3538562ea9eef5db2257f6df76ef7c055349d</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 12 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 12 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][617.41 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/fcc3538562ea9eef5db2257f6df76ef7c055349d</link>
|
||||||
|
<contentLength>647401280</contentLength>
|
||||||
|
<pubDate>2023-11-30T19:32:15.303</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="647401280"
|
||||||
|
url="https://mikanani.me/Download/20231130/fcc3538562ea9eef5db2257f6df76ef7c055349d.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 11 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/165dc4f903a0cd0dd44d40f52ec16d1e8ae9c3b8</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 11 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 11 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][613.83 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/165dc4f903a0cd0dd44d40f52ec16d1e8ae9c3b8</link>
|
||||||
|
<contentLength>643647424</contentLength>
|
||||||
|
<pubDate>2023-11-21T21:21:49.327</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="643647424"
|
||||||
|
url="https://mikanani.me/Download/20231121/165dc4f903a0cd0dd44d40f52ec16d1e8ae9c3b8.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 10v2
|
||||||
|
[WebRip 1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/6c96714831b0c3df6733c346e33443fef4104b5d</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 10v2 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 10v2 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][659.01 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/6c96714831b0c3df6733c346e33443fef4104b5d</link>
|
||||||
|
<contentLength>691022080</contentLength>
|
||||||
|
<pubDate>2023-11-16T08:43:00.108</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="691022080"
|
||||||
|
url="https://mikanani.me/Download/20231116/6c96714831b0c3df6733c346e33443fef4104b5d.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 10 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/94180479683a37d72c99cf2a488e54f734d473b6</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 10 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 10 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][659 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/94180479683a37d72c99cf2a488e54f734d473b6</link>
|
||||||
|
<contentLength>691011584</contentLength>
|
||||||
|
<pubDate>2023-11-15T22:05:30.234</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="691011584"
|
||||||
|
url="https://mikanani.me/Download/20231115/94180479683a37d72c99cf2a488e54f734d473b6.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 09 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/c9295353c47593697c45c2c206f772cc522b5f9c</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 09 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 09 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][713.36 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/c9295353c47593697c45c2c206f772cc522b5f9c</link>
|
||||||
|
<contentLength>748012160</contentLength>
|
||||||
|
<pubDate>2023-11-08T08:41:21.308</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="748012160"
|
||||||
|
url="https://mikanani.me/Download/20231108/c9295353c47593697c45c2c206f772cc522b5f9c.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 08 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/b7c1508c2d2b04b25bdc51535d81ea535741becf</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 08 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 08 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][615.25 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/b7c1508c2d2b04b25bdc51535d81ea535741becf</link>
|
||||||
|
<contentLength>645136384</contentLength>
|
||||||
|
<pubDate>2023-10-30T23:25:20.966</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="645136384"
|
||||||
|
url="https://mikanani.me/Download/20231030/b7c1508c2d2b04b25bdc51535d81ea535741becf.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 07 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/51092d51a5fe8180b10a97aeeba98f38d6fb8ba9</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 07 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 07 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][661.84 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/51092d51a5fe8180b10a97aeeba98f38d6fb8ba9</link>
|
||||||
|
<contentLength>693989568</contentLength>
|
||||||
|
<pubDate>2023-10-25T10:41:29.838</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="693989568"
|
||||||
|
url="https://mikanani.me/Download/20231025/51092d51a5fe8180b10a97aeeba98f38d6fb8ba9.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 06 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/5ee3c55378239568f12d4b113b2a55469b99e72c</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 06 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 06 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][694.38 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/5ee3c55378239568f12d4b113b2a55469b99e72c</link>
|
||||||
|
<contentLength>728110208</contentLength>
|
||||||
|
<pubDate>2023-10-16T23:01:22.685</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="728110208"
|
||||||
|
url="https://mikanani.me/Download/20231016/5ee3c55378239568f12d4b113b2a55469b99e72c.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 05 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/3b344756acd398a7b4e2485c10fa644b6dbaf0da</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 05 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 05 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][752.47 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/3b344756acd398a7b4e2485c10fa644b6dbaf0da</link>
|
||||||
|
<contentLength>789021952</contentLength>
|
||||||
|
<pubDate>2023-10-10T13:57:58.318</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="789021952"
|
||||||
|
url="https://mikanani.me/Download/20231010/3b344756acd398a7b4e2485c10fa644b6dbaf0da.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 04 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/496df298c3473ff8cd5efe3b67c8b67bcbd8b1e1</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 04 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 04 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][761.14 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/496df298c3473ff8cd5efe3b67c8b67bcbd8b1e1</link>
|
||||||
|
<contentLength>798113152</contentLength>
|
||||||
|
<pubDate>2023-10-08T19:12:30.906</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="798113152"
|
||||||
|
url="https://mikanani.me/Download/20231008/496df298c3473ff8cd5efe3b67c8b67bcbd8b1e1.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 03 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/14e1af5a8f13ec28f0d7c417bec8085c39c84798</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 03 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 03 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][759.95 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/14e1af5a8f13ec28f0d7c417bec8085c39c84798</link>
|
||||||
|
<contentLength>796865344</contentLength>
|
||||||
|
<pubDate>2023-10-07T21:41:11.038</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="796865344"
|
||||||
|
url="https://mikanani.me/Download/20231007/14e1af5a8f13ec28f0d7c417bec8085c39c84798.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 02 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/3e75bd5e08ce15846298d765122d4c3ec92b5abb</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 02 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 02 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][791.06 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/3e75bd5e08ce15846298d765122d4c3ec92b5abb</link>
|
||||||
|
<contentLength>829486528</contentLength>
|
||||||
|
<pubDate>2023-10-06T08:50:57.386</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="829486528"
|
||||||
|
url="https://mikanani.me/Download/20231006/3e75bd5e08ce15846298d765122d4c3ec92b5abb.torrent" />
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<guid isPermaLink="false">[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 01 [WebRip
|
||||||
|
1080p HEVC-10bit AAC][简繁日内封字幕]</guid>
|
||||||
|
<link>https://mikanani.me/Home/Episode/d17248aac2caffbcd06a2346c1863531ece4c158</link>
|
||||||
|
<title>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 01 [WebRip 1080p HEVC-10bit
|
||||||
|
AAC][简繁日内封字幕]</title>
|
||||||
|
<description>[喵萌奶茶屋&LoliHouse] 葬送的芙莉莲 / Sousou no Frieren - 01 [WebRip 1080p
|
||||||
|
HEVC-10bit AAC][简繁日内封字幕][753.71 MB]</description>
|
||||||
|
<torrent xmlns="https://mikanani.me/0.1/">
|
||||||
|
<link>https://mikanani.me/Home/Episode/d17248aac2caffbcd06a2346c1863531ece4c158</link>
|
||||||
|
<contentLength>790322240</contentLength>
|
||||||
|
<pubDate>2023-10-04T16:09:55.167</pubDate>
|
||||||
|
</torrent>
|
||||||
|
<enclosure type="application/x-bittorrent" length="790322240"
|
||||||
|
url="https://mikanani.me/Download/20231004/d17248aac2caffbcd06a2346c1863531ece4c158.torrent" />
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
1722
apps/recorder/tests/resources/mikan/Bangumi-3416.rss
Normal file
1722
apps/recorder/tests/resources/mikan/Bangumi-3416.rss
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user