fix: fix scrape mikan season bangumi list
This commit is contained in:
parent
439353d318
commit
a7f52fe0eb
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1956,6 +1956,7 @@ dependencies = [
|
|||||||
"serde_with",
|
"serde_with",
|
||||||
"snafu",
|
"snafu",
|
||||||
"url",
|
"url",
|
||||||
|
"util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2,7 +2,9 @@ use std::{fmt::Debug, ops::Deref, sync::Arc};
|
|||||||
|
|
||||||
use fetch::{HttpClient, HttpClientTrait};
|
use fetch::{HttpClient, HttpClientTrait};
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, DbErr, TryIntoModel};
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue::Set, ColumnTrait, DbErr, EntityTrait, QueryFilter, TryIntoModel,
|
||||||
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use util::OptDynErr;
|
use util::OptDynErr;
|
||||||
|
|
||||||
@ -137,7 +139,7 @@ impl MikanClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save_credential(
|
pub async fn submit_credential_form(
|
||||||
&self,
|
&self,
|
||||||
ctx: Arc<dyn AppContextTrait>,
|
ctx: Arc<dyn AppContextTrait>,
|
||||||
subscriber_id: i32,
|
subscriber_id: i32,
|
||||||
@ -159,49 +161,67 @@ impl MikanClient {
|
|||||||
Ok(credential)
|
Ok(credential)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn sync_credential_cookies(
|
||||||
|
&self,
|
||||||
|
ctx: Arc<dyn AppContextTrait>,
|
||||||
|
credential_id: i32,
|
||||||
|
) -> RecorderResult<()> {
|
||||||
|
let cookies = self.http_client.save_cookie_store_to_json()?;
|
||||||
|
if let Some(cookies) = cookies {
|
||||||
|
let am = credential_3rd::ActiveModel {
|
||||||
|
cookies: Set(Some(cookies)),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.try_encrypt(ctx.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
credential_3rd::Entity::update_many()
|
||||||
|
.set(am)
|
||||||
|
.filter(credential_3rd::Column::Id.eq(credential_id))
|
||||||
|
.exec(ctx.db())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn fork_with_credential(
|
pub async fn fork_with_credential(
|
||||||
&self,
|
&self,
|
||||||
ctx: Arc<dyn AppContextTrait>,
|
ctx: Arc<dyn AppContextTrait>,
|
||||||
credential_id: Option<i32>,
|
credential_id: i32,
|
||||||
) -> RecorderResult<Self> {
|
) -> RecorderResult<Self> {
|
||||||
let mut fork = self.http_client.fork();
|
let mut fork = self.http_client.fork();
|
||||||
let mut userpass_credential_opt = None;
|
|
||||||
|
|
||||||
if let Some(credential_id) = credential_id {
|
let credential = credential_3rd::Model::find_by_id(ctx.clone(), credential_id).await?;
|
||||||
let credential = credential_3rd::Model::find_by_id(ctx.clone(), credential_id).await?;
|
if let Some(credential) = credential {
|
||||||
if let Some(credential) = credential {
|
if credential.credential_type != Credential3rdType::Mikan {
|
||||||
if credential.credential_type != Credential3rdType::Mikan {
|
return Err(RecorderError::Credential3rdError {
|
||||||
return Err(RecorderError::Credential3rdError {
|
message: "credential is not a mikan credential".to_string(),
|
||||||
message: "credential is not a mikan credential".to_string(),
|
source: None.into(),
|
||||||
source: None.into(),
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let userpass_credential: UserPassCredential =
|
|
||||||
credential.try_into_userpass_credential(ctx)?;
|
|
||||||
|
|
||||||
if let Some(cookies) = userpass_credential.cookies.as_ref() {
|
|
||||||
fork = fork.attach_cookies(cookies)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(user_agent) = userpass_credential.user_agent.as_ref() {
|
|
||||||
fork = fork.attach_user_agent(user_agent);
|
|
||||||
}
|
|
||||||
|
|
||||||
userpass_credential_opt = Some(userpass_credential);
|
|
||||||
} else {
|
|
||||||
return Err(RecorderError::from_db_record_not_found(
|
|
||||||
DbErr::RecordNotFound(format!("credential={credential_id} not found")),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
let userpass_credential: UserPassCredential =
|
||||||
http_client: HttpClient::from_fork(fork)?,
|
credential.try_into_userpass_credential(ctx)?;
|
||||||
base_url: self.base_url.clone(),
|
|
||||||
origin_url: self.origin_url.clone(),
|
fork = fork.attach_cookies(userpass_credential.cookies.as_deref())?;
|
||||||
userpass_credential: userpass_credential_opt,
|
|
||||||
})
|
if let Some(user_agent) = userpass_credential.user_agent.as_ref() {
|
||||||
|
fork = fork.attach_user_agent(user_agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
let userpass_credential_opt = Some(userpass_credential);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
http_client: HttpClient::from_fork(fork)?,
|
||||||
|
base_url: self.base_url.clone(),
|
||||||
|
origin_url: self.origin_url.clone(),
|
||||||
|
userpass_credential: userpass_credential_opt,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(RecorderError::from_db_record_not_found(
|
||||||
|
DbErr::RecordNotFound(format!("credential={credential_id} not found")),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn base_url(&self) -> &Url {
|
pub fn base_url(&self) -> &Url {
|
||||||
|
@ -2,5 +2,7 @@ pub const MIKAN_POSTER_BUCKET_KEY: &str = "mikan_poster";
|
|||||||
pub const MIKAN_UNKNOWN_FANSUB_NAME: &str = "生肉/不明字幕";
|
pub const MIKAN_UNKNOWN_FANSUB_NAME: &str = "生肉/不明字幕";
|
||||||
pub const MIKAN_UNKNOWN_FANSUB_ID: &str = "202";
|
pub const MIKAN_UNKNOWN_FANSUB_ID: &str = "202";
|
||||||
pub const MIKAN_LOGIN_PAGE_PATH: &str = "/Account/Login";
|
pub const MIKAN_LOGIN_PAGE_PATH: &str = "/Account/Login";
|
||||||
pub const MIKAN_LOGIN_PAGE_SEARCH: &str = "?ReturnUrl=%2F";
|
pub const MIKAN_LOGIN_PAGE_SEARCH: &str = "ReturnUrl=%2F";
|
||||||
pub const MIKAN_ACCOUNT_MANAGE_PAGE_PATH: &str = "/Account/Manage";
|
pub const MIKAN_ACCOUNT_MANAGE_PAGE_PATH: &str = "/Account/Manage";
|
||||||
|
pub const MIKAN_SEASON_FLOW_PAGE_PATH: &str = "/Home/BangumiCoverFlow";
|
||||||
|
pub const MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH: &str = "/Home/ExpandBangumi";
|
||||||
|
@ -8,8 +8,9 @@ mod web;
|
|||||||
pub use client::MikanClient;
|
pub use client::MikanClient;
|
||||||
pub use config::MikanConfig;
|
pub use config::MikanConfig;
|
||||||
pub use constants::{
|
pub use constants::{
|
||||||
MIKAN_ACCOUNT_MANAGE_PAGE_PATH, MIKAN_LOGIN_PAGE_PATH, MIKAN_LOGIN_PAGE_SEARCH,
|
MIKAN_ACCOUNT_MANAGE_PAGE_PATH, MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH,
|
||||||
MIKAN_POSTER_BUCKET_KEY, MIKAN_UNKNOWN_FANSUB_ID, MIKAN_UNKNOWN_FANSUB_NAME,
|
MIKAN_LOGIN_PAGE_PATH, MIKAN_LOGIN_PAGE_SEARCH, MIKAN_POSTER_BUCKET_KEY,
|
||||||
|
MIKAN_SEASON_FLOW_PAGE_PATH, MIKAN_UNKNOWN_FANSUB_ID, MIKAN_UNKNOWN_FANSUB_NAME,
|
||||||
};
|
};
|
||||||
pub use credential::MikanCredentialForm;
|
pub use credential::MikanCredentialForm;
|
||||||
pub use rss::{
|
pub use rss::{
|
||||||
|
@ -10,7 +10,8 @@ use tracing::instrument;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
MIKAN_POSTER_BUCKET_KEY, MikanBangumiRssUrlMeta, MikanClient,
|
MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH, MIKAN_POSTER_BUCKET_KEY,
|
||||||
|
MIKAN_SEASON_FLOW_PAGE_PATH, MikanBangumiRssUrlMeta, MikanClient,
|
||||||
extract_mikan_bangumi_id_from_rss_url,
|
extract_mikan_bangumi_id_from_rss_url,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -183,7 +184,7 @@ pub fn build_mikan_season_flow_url(
|
|||||||
season_str: MikanSeasonStr,
|
season_str: MikanSeasonStr,
|
||||||
) -> Url {
|
) -> Url {
|
||||||
let mut url = mikan_base_url;
|
let mut url = mikan_base_url;
|
||||||
url.set_path("/Home/BangumiCoverFlow");
|
url.set_path(MIKAN_SEASON_FLOW_PAGE_PATH);
|
||||||
url.query_pairs_mut()
|
url.query_pairs_mut()
|
||||||
.append_pair("year", &year.to_string())
|
.append_pair("year", &year.to_string())
|
||||||
.append_pair("seasonStr", &season_str.to_string());
|
.append_pair("seasonStr", &season_str.to_string());
|
||||||
@ -201,7 +202,7 @@ pub fn build_mikan_bangumi_expand_subscribed_url(
|
|||||||
mikan_bangumi_id: &str,
|
mikan_bangumi_id: &str,
|
||||||
) -> Url {
|
) -> Url {
|
||||||
let mut url = mikan_base_url;
|
let mut url = mikan_base_url;
|
||||||
url.set_path("/ExpandBangumi");
|
url.set_path(MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH);
|
||||||
url.query_pairs_mut()
|
url.query_pairs_mut()
|
||||||
.append_pair("bangumiId", mikan_bangumi_id)
|
.append_pair("bangumiId", mikan_bangumi_id)
|
||||||
.append_pair("showSubscribed", "true");
|
.append_pair("showSubscribed", "true");
|
||||||
@ -651,7 +652,7 @@ pub async fn scrape_mikan_bangumi_meta_list_from_season_flow_url(
|
|||||||
credential_id: i32,
|
credential_id: i32,
|
||||||
) -> RecorderResult<Vec<MikanBangumiMeta>> {
|
) -> RecorderResult<Vec<MikanBangumiMeta>> {
|
||||||
let mikan_client = mikan_client
|
let mikan_client = mikan_client
|
||||||
.fork_with_credential(ctx.clone(), Some(credential_id))
|
.fork_with_credential(ctx.clone(), credential_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mikan_base_url = mikan_client.base_url();
|
let mikan_base_url = mikan_client.base_url();
|
||||||
@ -671,6 +672,10 @@ pub async fn scrape_mikan_bangumi_meta_list_from_season_flow_url(
|
|||||||
|
|
||||||
let mut bangumi_metas = vec![];
|
let mut bangumi_metas = vec![];
|
||||||
|
|
||||||
|
mikan_client
|
||||||
|
.sync_credential_cookies(ctx.clone(), credential_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
for bangumi_index in bangumi_indices_meta {
|
for bangumi_index in bangumi_indices_meta {
|
||||||
let bangumi_title = bangumi_index.bangumi_title.clone();
|
let bangumi_title = bangumi_index.bangumi_title.clone();
|
||||||
let bangumi_expand_subscribed_fragment_url = build_mikan_bangumi_expand_subscribed_url(
|
let bangumi_expand_subscribed_fragment_url = build_mikan_bangumi_expand_subscribed_url(
|
||||||
@ -696,6 +701,10 @@ pub async fn scrape_mikan_bangumi_meta_list_from_season_flow_url(
|
|||||||
bangumi_metas.push(bangumi_meta);
|
bangumi_metas.push(bangumi_meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mikan_client
|
||||||
|
.sync_credential_cookies(ctx, credential_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(bangumi_metas)
|
Ok(bangumi_metas)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -704,7 +713,6 @@ mod test {
|
|||||||
#![allow(unused_variables)]
|
#![allow(unused_variables)]
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use fetch::get_random_ua;
|
|
||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@ -712,11 +720,16 @@ mod test {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
extract::mikan::MikanCredentialForm,
|
extract::mikan::{MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH, MIKAN_SEASON_FLOW_PAGE_PATH},
|
||||||
test_utils::{
|
test_utils::{
|
||||||
app::UnitTestAppContext, crypto::build_testing_crypto_service,
|
app::UnitTestAppContext,
|
||||||
database::build_testing_database_service, mikan::build_testing_mikan_client,
|
crypto::build_testing_crypto_service,
|
||||||
storage::build_testing_storage_service, tracing::try_init_testing_tracing,
|
database::build_testing_database_service,
|
||||||
|
mikan::{
|
||||||
|
MikanMockServer, build_testing_mikan_client, build_testing_mikan_credential_form,
|
||||||
|
},
|
||||||
|
storage::build_testing_storage_service,
|
||||||
|
tracing::try_init_testing_tracing,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -932,8 +945,8 @@ mod test {
|
|||||||
async fn test_scrape_mikan_bangumi_meta_list_from_season_flow_url(
|
async fn test_scrape_mikan_bangumi_meta_list_from_season_flow_url(
|
||||||
before_each: (),
|
before_each: (),
|
||||||
) -> RecorderResult<()> {
|
) -> RecorderResult<()> {
|
||||||
let mut mikan_server = mockito::Server::new_async().await;
|
let mut mikan_server = MikanMockServer::new().await?;
|
||||||
let mikan_base_url = Url::parse(&mikan_server.url())?;
|
let mikan_base_url = mikan_server.base_url().clone();
|
||||||
|
|
||||||
let app_ctx = {
|
let app_ctx = {
|
||||||
let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?;
|
let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?;
|
||||||
@ -950,20 +963,50 @@ mod test {
|
|||||||
|
|
||||||
let mikan_client = app_ctx.mikan();
|
let mikan_client = app_ctx.mikan();
|
||||||
|
|
||||||
|
let login_mock = mikan_server.mock_get_login_page();
|
||||||
|
|
||||||
|
let season_flow_noauth_mock = mikan_server
|
||||||
|
.server
|
||||||
|
.mock("GET", MIKAN_SEASON_FLOW_PAGE_PATH)
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.match_request(|req| !MikanMockServer::get_has_auth_matcher()(req))
|
||||||
|
.with_status(200)
|
||||||
|
.with_body_from_file("tests/resources/mikan/BangumiCoverFlow-2025-spring-noauth.html")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let season_flow_mock = mikan_server
|
||||||
|
.server
|
||||||
|
.mock("GET", MIKAN_SEASON_FLOW_PAGE_PATH)
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.match_request(|req| MikanMockServer::get_has_auth_matcher()(req))
|
||||||
|
.with_status(200)
|
||||||
|
.with_body_from_file("tests/resources/mikan/BangumiCoverFlow-2025-spring.html")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let bangumi_subscribed_noauth_mock = mikan_server
|
||||||
|
.server
|
||||||
|
.mock("GET", MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH)
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.match_request(|req| !MikanMockServer::get_has_auth_matcher()(req))
|
||||||
|
.with_status(200)
|
||||||
|
.with_body_from_file("tests/resources/mikan/ExpandBangumi-3599-noauth.html")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let bangumi_subscribed_mock = mikan_server
|
||||||
|
.server
|
||||||
|
.mock("GET", MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH)
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.match_request(|req| MikanMockServer::get_has_auth_matcher()(req))
|
||||||
|
.with_status(200)
|
||||||
|
.with_body_from_file("tests/resources/mikan/ExpandBangumi-3599.html")
|
||||||
|
.create();
|
||||||
|
|
||||||
let credential = mikan_client
|
let credential = mikan_client
|
||||||
.save_credential(
|
.submit_credential_form(app_ctx.clone(), 1, build_testing_mikan_credential_form())
|
||||||
app_ctx.clone(),
|
|
||||||
1,
|
|
||||||
MikanCredentialForm {
|
|
||||||
username: String::from("test_username"),
|
|
||||||
password: String::from("test_password"),
|
|
||||||
user_agent: get_random_ua().to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mikan_season_flow_url =
|
let mikan_season_flow_url =
|
||||||
build_mikan_season_flow_url(mikan_base_url, 2025, MikanSeasonStr::Spring);
|
build_mikan_season_flow_url(mikan_base_url.clone(), 2025, MikanSeasonStr::Spring);
|
||||||
|
|
||||||
let bangumi_meta_list = scrape_mikan_bangumi_meta_list_from_season_flow_url(
|
let bangumi_meta_list = scrape_mikan_bangumi_meta_list_from_season_flow_url(
|
||||||
mikan_client,
|
mikan_client,
|
||||||
@ -975,6 +1018,26 @@ mod test {
|
|||||||
|
|
||||||
assert!(!bangumi_meta_list.is_empty());
|
assert!(!bangumi_meta_list.is_empty());
|
||||||
|
|
||||||
|
let bangumi = bangumi_meta_list.first().unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
bangumi
|
||||||
|
.homepage
|
||||||
|
.to_string()
|
||||||
|
.ends_with("/Home/Bangumi/3288#370"),
|
||||||
|
);
|
||||||
|
assert_eq!(bangumi.bangumi_title, "吉伊卡哇");
|
||||||
|
assert_eq!(bangumi.mikan_bangumi_id, "3288");
|
||||||
|
assert!(
|
||||||
|
bangumi
|
||||||
|
.origin_poster_src
|
||||||
|
.as_ref()
|
||||||
|
.map_or(String::new(), |u| u.to_string())
|
||||||
|
.ends_with("/images/Bangumi/202204/d8ef46c0.jpg")
|
||||||
|
);
|
||||||
|
assert_eq!(bangumi.mikan_fansub_id, String::from("370"));
|
||||||
|
assert_eq!(bangumi.fansub, String::from("LoliHouse"));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -987,8 +1050,9 @@ mod test {
|
|||||||
let mikan_base_url = Url::parse(&mikan_server.url())?;
|
let mikan_base_url = Url::parse(&mikan_server.url())?;
|
||||||
let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?;
|
let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?;
|
||||||
|
|
||||||
let episode_homepage_url =
|
let episode_homepage_url = mikan_base_url
|
||||||
mikan_base_url.join("/Home/Episode/475184dce83ea2b82902592a5ac3343f6d54b36a")?;
|
.clone()
|
||||||
|
.join("/Home/Episode/475184dce83ea2b82902592a5ac3343f6d54b36a")?;
|
||||||
|
|
||||||
let episode_homepage_mock = mikan_server
|
let episode_homepage_mock = mikan_server
|
||||||
.mock("GET", episode_homepage_url.path())
|
.mock("GET", episode_homepage_url.path())
|
||||||
@ -1058,101 +1122,4 @@ mod test {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[rstest]
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn test_extract_mikan_bangumis_meta_from_my_bangumi_page(
|
|
||||||
// before_each: (),
|
|
||||||
// ) -> RecorderResult<()> {
|
|
||||||
// let mut mikan_server = mockito::Server::new_async().await;
|
|
||||||
|
|
||||||
// let mikan_base_url = Url::parse(&mikan_server.url())?;
|
|
||||||
|
|
||||||
// let my_bangumi_page_url = mikan_base_url.join("/Home/MyBangumi")?;
|
|
||||||
|
|
||||||
// let context = Arc::new(
|
|
||||||
// UnitTestAppContext::builder()
|
|
||||||
//
|
|
||||||
// .mikan(build_testing_mikan_client(mikan_base_url.clone()).await?)
|
|
||||||
// .build(),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// {
|
|
||||||
// let my_bangumi_without_cookie_mock = mikan_server
|
|
||||||
// .mock("GET", my_bangumi_page_url.path())
|
|
||||||
// .match_header(header::COOKIE, mockito::Matcher::Missing)
|
|
||||||
//
|
|
||||||
// .with_body_from_file("tests/resources/mikan/MyBangumi-noauth.htm")
|
|
||||||
// .create_async()
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// let bangumi_metas =
|
|
||||||
// extract_mikan_bangumis_meta_from_my_bangumi_page(
|
|
||||||
// context.clone(), my_bangumi_page_url.clone(),
|
|
||||||
// None,
|
|
||||||
// &[],
|
|
||||||
// );
|
|
||||||
|
|
||||||
// pin_mut!(bangumi_metas);
|
|
||||||
|
|
||||||
// let bangumi_metas = bangumi_metas.try_collect::<Vec<_>>().await?;
|
|
||||||
|
|
||||||
// assert!(bangumi_metas.is_empty());
|
|
||||||
|
|
||||||
// assert!(my_bangumi_without_cookie_mock.matched_async().await);
|
|
||||||
// }
|
|
||||||
// {
|
|
||||||
// let my_bangumi_with_cookie_mock = mikan_server
|
|
||||||
// .mock("GET", my_bangumi_page_url.path())
|
|
||||||
// .match_header(
|
|
||||||
// header::COOKIE,
|
|
||||||
// mockito::Matcher::AllOf(vec![
|
|
||||||
//
|
|
||||||
// mockito::Matcher::Regex(String::from(".*\\.AspNetCore\\.Antiforgery.*")),
|
|
||||||
// mockito::Matcher::Regex(String::from(
|
|
||||||
// ".*\\.AspNetCore\\.Identity\\.Application.*",
|
|
||||||
// )),
|
|
||||||
// ]),
|
|
||||||
// )
|
|
||||||
// .with_body_from_file("tests/resources/mikan/MyBangumi.htm")
|
|
||||||
// .create_async()
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// let expand_bangumi_mock = mikan_server
|
|
||||||
// .mock("GET", "/ExpandBangumi")
|
|
||||||
// .match_query(mockito::Matcher::Any)
|
|
||||||
//
|
|
||||||
// .with_body_from_file("tests/resources/mikan/ExpandBangumi.htm")
|
|
||||||
// .create_async()
|
|
||||||
// .await;
|
|
||||||
|
|
||||||
// let auth_secrecy = Some(MikanCredentialForm {
|
|
||||||
// username: String::from("test_username"),
|
|
||||||
// password: String::from("test_password"),
|
|
||||||
// user_agent: String::from(
|
|
||||||
// "Mozilla/5.0 (Windows NT 10.0; Win64; x64)
|
|
||||||
// AppleWebKit/537.36 (KHTML, like \ Gecko)
|
|
||||||
// Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0", ),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// let bangumi_metas =
|
|
||||||
// extract_mikan_bangumis_meta_from_my_bangumi_page(
|
|
||||||
// context.clone(), my_bangumi_page_url,
|
|
||||||
// auth_secrecy,
|
|
||||||
// &[],
|
|
||||||
// );
|
|
||||||
// pin_mut!(bangumi_metas);
|
|
||||||
// let bangumi_metas = bangumi_metas.try_collect::<Vec<_>>().await?;
|
|
||||||
|
|
||||||
// assert!(!bangumi_metas.is_empty());
|
|
||||||
|
|
||||||
// assert!(bangumi_metas[0].origin_poster_src.is_some());
|
|
||||||
|
|
||||||
// assert!(my_bangumi_with_cookie_mock.matched_async().await);
|
|
||||||
|
|
||||||
// expand_bangumi_mock.expect(bangumi_metas.len());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,22 @@
|
|||||||
use fetch::{FetchError, HttpClientConfig, IntoUrl};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use fetch::{FetchError, HttpClientConfig, IntoUrl, get_random_ua};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::RecorderResult,
|
errors::RecorderResult,
|
||||||
extract::mikan::{MikanClient, MikanConfig},
|
extract::mikan::{
|
||||||
|
MIKAN_ACCOUNT_MANAGE_PAGE_PATH, MIKAN_LOGIN_PAGE_PATH, MikanClient, MikanConfig,
|
||||||
|
MikanCredentialForm,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TESTING_MIKAN_USERNAME: &str = "test_username";
|
||||||
|
const TESTING_MIKAN_PASSWORD: &str = "test_password";
|
||||||
|
const TESTING_MIKAN_ANTIFORGERY: &str = "test_antiforgery";
|
||||||
|
const TESTING_MIKAN_IDENTITY: &str = "test_identity";
|
||||||
|
|
||||||
pub async fn build_testing_mikan_client(
|
pub async fn build_testing_mikan_client(
|
||||||
base_mikan_url: impl IntoUrl,
|
base_mikan_url: impl IntoUrl,
|
||||||
) -> RecorderResult<MikanClient> {
|
) -> RecorderResult<MikanClient> {
|
||||||
@ -17,3 +29,145 @@ pub async fn build_testing_mikan_client(
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(mikan_client)
|
Ok(mikan_client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_testing_mikan_credential_form() -> MikanCredentialForm {
|
||||||
|
MikanCredentialForm {
|
||||||
|
username: String::from(TESTING_MIKAN_USERNAME),
|
||||||
|
password: String::from(TESTING_MIKAN_PASSWORD),
|
||||||
|
user_agent: get_random_ua().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MikanMockServerLoginMock {
|
||||||
|
pub login_get_mock: mockito::Mock,
|
||||||
|
pub login_post_success_mock: mockito::Mock,
|
||||||
|
pub login_post_failed_mock: mockito::Mock,
|
||||||
|
pub account_get_success_mock: mockito::Mock,
|
||||||
|
pub account_get_failed_mock: mockito::Mock,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MikanMockServer {
|
||||||
|
pub server: mockito::ServerGuard,
|
||||||
|
base_url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MikanMockServer {
|
||||||
|
pub async fn new() -> RecorderResult<Self> {
|
||||||
|
let server = mockito::Server::new_async().await;
|
||||||
|
let base_url = Url::parse(&server.url())?;
|
||||||
|
|
||||||
|
Ok(Self { server, base_url })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn base_url(&self) -> &Url {
|
||||||
|
&self.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_has_auth_matcher() -> impl Fn(&mockito::Request) -> bool {
|
||||||
|
|req: &mockito::Request| -> bool {
|
||||||
|
let test_identity_cookie =
|
||||||
|
format!(".AspNetCore.Identity.Application={TESTING_MIKAN_IDENTITY}");
|
||||||
|
req.header("Cookie").iter().any(|cookie| {
|
||||||
|
cookie
|
||||||
|
.to_str()
|
||||||
|
.is_ok_and(|c| c.contains(&test_identity_cookie))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mock_get_login_page(&mut self) -> MikanMockServerLoginMock {
|
||||||
|
let login_get_mock = self
|
||||||
|
.server
|
||||||
|
.mock("GET", MIKAN_LOGIN_PAGE_PATH)
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.with_status(201)
|
||||||
|
.with_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
.with_header(
|
||||||
|
"Set-Cookie",
|
||||||
|
&format!(
|
||||||
|
".AspNetCore.Antiforgery.test_app_id={TESTING_MIKAN_ANTIFORGERY}; HttpOnly; \
|
||||||
|
SameSite=Strict; Path=/"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let test_identity_expires = (Utc::now() + Duration::days(30)).to_rfc2822();
|
||||||
|
|
||||||
|
let match_post_login_body = |req: &mockito::Request| {
|
||||||
|
req.body()
|
||||||
|
.map(|b| url::form_urlencoded::parse(b))
|
||||||
|
.is_ok_and(|queires| {
|
||||||
|
let qs = queires.collect::<HashMap<_, _>>();
|
||||||
|
qs.get("UserName")
|
||||||
|
.is_some_and(|s| s == TESTING_MIKAN_USERNAME)
|
||||||
|
&& qs
|
||||||
|
.get("Password")
|
||||||
|
.is_some_and(|s| s == TESTING_MIKAN_PASSWORD)
|
||||||
|
&& qs
|
||||||
|
.get("__RequestVerificationToken")
|
||||||
|
.is_some_and(|s| s == TESTING_MIKAN_ANTIFORGERY)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let login_post_success_mock = {
|
||||||
|
let mikan_base_url = self.base_url().clone();
|
||||||
|
self.server
|
||||||
|
.mock("POST", MIKAN_LOGIN_PAGE_PATH)
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.match_request(match_post_login_body)
|
||||||
|
.with_status(302)
|
||||||
|
.with_header(
|
||||||
|
"Set-Cookie",
|
||||||
|
&format!(
|
||||||
|
".AspNetCore.Identity.Application={TESTING_MIKAN_IDENTITY}; HttpOnly; \
|
||||||
|
SameSite=Lax; Path=/; Expires=${test_identity_expires}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_header_from_request("Location", move |req| {
|
||||||
|
let request_url = mikan_base_url.join(req.path_and_query()).ok();
|
||||||
|
request_url
|
||||||
|
.and_then(|u| {
|
||||||
|
u.query_pairs()
|
||||||
|
.find(|(key, _)| key == "ReturnUrl")
|
||||||
|
.map(|(_, value)| value.to_string())
|
||||||
|
})
|
||||||
|
.unwrap_or(String::from("/"))
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
};
|
||||||
|
|
||||||
|
let login_post_failed_mock = self
|
||||||
|
.server
|
||||||
|
.mock("POST", MIKAN_LOGIN_PAGE_PATH)
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.match_request(move |req| !match_post_login_body(req))
|
||||||
|
.with_status(200)
|
||||||
|
.with_body_from_file("tests/resources/mikan/LoginError.html")
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let account_get_success_mock = self
|
||||||
|
.server
|
||||||
|
.mock("GET", MIKAN_ACCOUNT_MANAGE_PAGE_PATH)
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.match_request(move |req| Self::get_has_auth_matcher()(req))
|
||||||
|
.with_status(200)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
let account_get_failed_mock = self
|
||||||
|
.server
|
||||||
|
.mock("GET", MIKAN_ACCOUNT_MANAGE_PAGE_PATH)
|
||||||
|
.match_query(mockito::Matcher::Any)
|
||||||
|
.match_request(move |req| !Self::get_has_auth_matcher()(req))
|
||||||
|
.with_status(302)
|
||||||
|
.with_header("Location", MIKAN_LOGIN_PAGE_PATH)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
MikanMockServerLoginMock {
|
||||||
|
login_get_mock,
|
||||||
|
login_post_success_mock,
|
||||||
|
login_post_failed_mock,
|
||||||
|
account_get_success_mock,
|
||||||
|
account_get_failed_mock,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
564
apps/recorder/tests/resources/mikan/LoginError.html
Normal file
564
apps/recorder/tests/resources/mikan/LoginError.html
Normal file
@ -0,0 +1,564 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<meta name="keywords" content="新番,动漫,动漫下載,新番下载,animation,bangumi,动画,蜜柑计划,Mikan Project" />
|
||||||
|
<meta name="description" content="蜜柑计划:新一代的动漫下载站" />
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<!-- 若用户有Google Chrome Frame,那么ie浏览时让IE使用chrome内核 -->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
|
||||||
|
<!-- 若是双核浏览器,默认webkit渲染(chrome) -->
|
||||||
|
<meta name="renderer" content="webkit">
|
||||||
|
<title>Mikan Project - 用户登录</title>
|
||||||
|
|
||||||
|
<!-- here put import css lib -->
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="/lib/bootstrap/dist/css/bootstrap.min.css?v=7s5uDGW3AHqw6xtJmNNtr-OBRJUlgkNJEo78P4b0yRw" />
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="/lib/font-awesome/css/font-awesome.min.css?v=3dkvEK0WLHRJ7_Csr0BZjAWxERc5WH7bdeUya2aXxdU" />
|
||||||
|
<link rel="stylesheet" href="/css/thirdparty.min.css?v=c2SZy6n-55iljz60XCAALXejEZvjc43kgwamU5DAYUU" />
|
||||||
|
<link rel="stylesheet" href="/css/animate.min.css?v=w_eXqGX0NdMPQ0LZNhdQ8B-DQMYAxelvLoIP39dzmus" />
|
||||||
|
<link rel="stylesheet" href="/css/mikan.min.css?v=aupBMgBgKRB5chTb5fl8lvHpN3OqX67_gKg3lXZewRw" />
|
||||||
|
|
||||||
|
<script src="/lib/jquery/dist/jquery.min.js?v=BbhdlvQf_xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44"></script>
|
||||||
|
<script src="/lib/bootstrap/dist/js/bootstrap.min.js?v=KXn5puMvxCw-dAYznun-drMdG1IFl3agK0p_pqT9KAo"></script>
|
||||||
|
<script src="/js/thirdparty.min.js?v=NsK_w5fw7Nm4ZPm4eZDgsivasZNgT6ArhIjmj-bRnR0"></script>
|
||||||
|
<script src="/js/darkreader.min.js?v=Lr_8XODLEDSPtT6LqaeLKzREs4jocJUzV8HvQPItIic"></script>
|
||||||
|
<script src="/js/ScrollMagic.min.js?v=1xuIM3UJWEZX_wWN9zrA8W7CWukfsMaEqb759CeHo3U"></script>
|
||||||
|
<script src="/js/jquery.ScrollMagic.min.js?v=SyygQh9gWWfvyS13QwI0SKGAQyHDachlaigiK4X59iw"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="icon" href="/images/favicon.ico?v=2" />
|
||||||
|
<link rel="apple-touch-icon" href="\Images\apple-touch-icon.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="152x152" href="\Images\apple-touch-icon-152x152.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="\Images\apple-touch-icon-180x180.png">
|
||||||
|
<link rel="apple-touch-icon" sizes="144x144" href="\Images\apple-touch-icon-144x144.png">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function (i, s, o, g, r, a, m) {
|
||||||
|
i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
|
||||||
|
(i[r].q = i[r].q || []).push(arguments)
|
||||||
|
}, i[r].l = 1 * new Date(); a = s.createElement(o),
|
||||||
|
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
|
||||||
|
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
|
||||||
|
|
||||||
|
ga('create', 'UA-8911610-8', 'auto');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="main">
|
||||||
|
<div id="sk-header" class="hidden-xs hidden-sm">
|
||||||
|
<div id="sk-top-nav" class="container">
|
||||||
|
<a id="logo" href="/" style="width:205px;"><img id="mikan-pic" src="/images/mikan-pic.png" /><img
|
||||||
|
src="/images/mikan-text.svg" style="height:30px;" /></a>
|
||||||
|
<div id="nav-list">
|
||||||
|
<ul class="list-inline nav-ul">
|
||||||
|
<li class="">
|
||||||
|
<div class="sk-col"><a href="/"><i class="fa fa-home fa-lg"></i>主页</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="">
|
||||||
|
<div class="sk-col"><a href="/Home/MyBangumi"><i class="fa fa-rss fa-lg"></i>订阅</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="">
|
||||||
|
<div class="sk-col"><a href="/Home/Classic"><i class="fa fa-slack fa-lg"></i>列表</a></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="search-form">
|
||||||
|
<form method="get" action="/Home/Search">
|
||||||
|
<div class="form-group has-feedback">
|
||||||
|
<label for="search" class="sr-only">搜索</label>
|
||||||
|
<input type="text" class="form-control input-sm" name="searchstr" id="header-search"
|
||||||
|
placeholder="搜索">
|
||||||
|
<span class="glyphicon glyphicon-search form-control-feedback"></span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<section id="login">
|
||||||
|
<div id="user-login" class="pull-right">
|
||||||
|
<a href="/Account/Register" class="text-right">注册</a>
|
||||||
|
<a onclick="ToggleActive(this)" class="text-right" data-toggle="popover-x"
|
||||||
|
data-target="#login-popover" data-placement="bottom bottom-right" rel="popover">登录</a>
|
||||||
|
<form action="/Account/Login?ReturnUrl=%2FAccount%2FLogin" class="form-vertical" method="post">
|
||||||
|
<div id="login-popover" class="popover popover-default">
|
||||||
|
<div class="arrow"></div>
|
||||||
|
<div id="login-popover-conent">
|
||||||
|
<div id="login-popover-input">
|
||||||
|
<div id="login-popover-div-username">
|
||||||
|
<img src="/images/user-name_login_icon.png" />
|
||||||
|
<input type="text" placeholder="用户名" id="login-popover-input-username"
|
||||||
|
name="UserName" />
|
||||||
|
</div>
|
||||||
|
<div id="login-popover-div-password">
|
||||||
|
<img src="/images/password_login_icon.png" style="margin-left:3px;" />
|
||||||
|
<input type="password" placeholder="密码" id="login-popover-input-password"
|
||||||
|
name="Password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="login-popover-submit" type="submit"
|
||||||
|
class="btn">登 录</button>
|
||||||
|
<div class="checkbox" id="login-popover-password">
|
||||||
|
<label id="login-popover-remember-password"><input type="checkbox" value="true"
|
||||||
|
name="RememberMe"><input type="hidden" value="false"
|
||||||
|
name="RememberMe">记住密码</label>
|
||||||
|
<div id="login-popover-forget-password" class="pull-right"><a
|
||||||
|
href="/Account/ForgotPassword" class="forget-password">忘记密码</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a id="login-popover-create-account">还没有账号?赶紧来注册一个吧~</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input name="__RequestVerificationToken" type="hidden"
|
||||||
|
value="CfDJ8MyNMqFNaC9JmJW13PvY-91NCe3U_rFij5nf6ni2NtxaxhQNSuonDbK7198YMfFdErEyhk-CHiByyDQaq371N3GUx0c8Xma0F0F2J2UQaszZgfjT5vxTV4O4viF6YoPDWMO2yLbeN7ok83_uz1DD-nU" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var AdvancedSubscriptionEnabled = false;
|
||||||
|
</script>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="ribbon">
|
||||||
|
<span class="ribbon-color1"></span>
|
||||||
|
<span class="ribbon-color2"></span>
|
||||||
|
<span class="ribbon-color3"></span>
|
||||||
|
<span class="ribbon-color4"></span>
|
||||||
|
<span class="ribbon-color5"></span>
|
||||||
|
<span class="ribbon-color6"></span>
|
||||||
|
<span class="ribbon-color7"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-home-nav hidden-lg hidden-md" id="sk-mobile-header">
|
||||||
|
<div class="m-home-tool-left clickable" data-toggle="modal" data-target="#modal-nav">
|
||||||
|
<i class="fa fa-bars" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="m-home-tool-left"></div>
|
||||||
|
<div style="text-align: center; height:100%;flex:1;">
|
||||||
|
<a href="/" style="text-decoration:none">
|
||||||
|
<img src="/images/mikan-pic.png" style="height: 3rem;margin-top: 0.5rem;">
|
||||||
|
<img src="/images/mikan-text.png" style="height: 1.5rem;margin-top: 0.5rem;">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="m-home-tool-right clickable" data-toggle="modal" data-target="#modal-login">
|
||||||
|
<i class="fa fa-user" aria-hidden="true" style="margin-right: 1rem;"></i>
|
||||||
|
</div>
|
||||||
|
<div class="m-home-tool-right clickable" onclick="ShowNavSearch()">
|
||||||
|
<i class="fa fa-search" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="m-nav-search" style="width: 100%;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<form method="get" action="/Home/Search">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon" id="sizing-addon1" style="border: none;background-color: white;">
|
||||||
|
<i class="fa fa-search" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control" placeholder="搜索" name="searchstr"
|
||||||
|
aria-describedby="sizing-addon1" style="border: none;font-size:16px;">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div style="width: 4rem;" onclick="HideNavSearch()">
|
||||||
|
<span style="font-size: 1.25rem;">取消</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media only screen and (min-device-width : 768px) {
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="account-bg-wrapper" class="hidden-sm hidden-xs">
|
||||||
|
<div class="logmod">
|
||||||
|
<div class="logmod__wrapper">
|
||||||
|
<div class="logmod__container">
|
||||||
|
<ul class="logmod__tabs">
|
||||||
|
<li data-tabtar="lgm-1"><a href="#">Mikan 账号注册</a></li>
|
||||||
|
<li data-tabtar="lgm-2"><a href="#">登录</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="logmod__tab-wrapper">
|
||||||
|
<div class="logmod__tab lgm-1">
|
||||||
|
<div class="logmod__heading">
|
||||||
|
<span class="logmod__heading-subtitle"></span>
|
||||||
|
</div>
|
||||||
|
<div class="logmod__form">
|
||||||
|
<p style="color:red" class="js-login-error">
|
||||||
|
登录失败,请重试.</p>
|
||||||
|
<form action="/Account/Register?ReturnUrl=%2FAccount%2FManage" class="simform"
|
||||||
|
id="registerForm" method="post">
|
||||||
|
<div class="logmod__inputs full ">
|
||||||
|
<input type="text" class="logmod-input-control" placeholder="用户名*"
|
||||||
|
name="UserName" />
|
||||||
|
</div>
|
||||||
|
<div class="logmod__inputs full">
|
||||||
|
<input type="password" class="logmod-input-control" placeholder="设置密码*"
|
||||||
|
name="Password" id="register-password" />
|
||||||
|
</div>
|
||||||
|
<div class="logmod__inputs full">
|
||||||
|
<input type="password" class="logmod-input-control" placeholder="确认密码*"
|
||||||
|
name="ConfirmPassword" />
|
||||||
|
</div>
|
||||||
|
<div class="logmod__inputs full">
|
||||||
|
<input type="text" class="logmod-input-control" placeholder="设置邮箱*"
|
||||||
|
name="Email" />
|
||||||
|
</div>
|
||||||
|
<div class="logmod__inputs full">
|
||||||
|
<input type="text" class="logmod-input-control" placeholder="QQ" name="QQ" />
|
||||||
|
</div>
|
||||||
|
<button class="logmod-submit btn" type="submit"
|
||||||
|
value="Register">注 册</button>
|
||||||
|
<div class="checkbox" id="logmod-password">
|
||||||
|
<div id="logmod-forget-password" class="pull-right"><a
|
||||||
|
href="/Account/ForgotPassword" class="forget-password">忘记密码</a></div>
|
||||||
|
</div>
|
||||||
|
<input name="__RequestVerificationToken" type="hidden"
|
||||||
|
value="CfDJ8MyNMqFNaC9JmJW13PvY-91NCe3U_rFij5nf6ni2NtxaxhQNSuonDbK7198YMfFdErEyhk-CHiByyDQaq371N3GUx0c8Xma0F0F2J2UQaszZgfjT5vxTV4O4viF6YoPDWMO2yLbeN7ok83_uz1DD-nU" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="logmod__tab lgm-2">
|
||||||
|
<div class="logmod__heading">
|
||||||
|
<span class="logmod__heading-subtitle">Hi,欢迎回来!</span>
|
||||||
|
</div>
|
||||||
|
<div class="logmod__form">
|
||||||
|
<p style="color:red" class="js-login-error">
|
||||||
|
登录失败,请重试.</p>
|
||||||
|
<form action="/Account/Login?ReturnUrl=%2FAccount%2FManage" class="simform"
|
||||||
|
id="loginForm" method="post">
|
||||||
|
<div class="logmod__inputs full left-addon">
|
||||||
|
<img src="/images/user-name_login_icon.png" class="logmod-icon" />
|
||||||
|
<input type="text" class="logmod-input-control" placeholder="用户名"
|
||||||
|
name="UserName" />
|
||||||
|
</div>
|
||||||
|
<div class="logmod__inputs full left-addon">
|
||||||
|
<img src="/images/password_login_icon.png" class="logmod-icon password" />
|
||||||
|
<input type="password" class="logmod-input-control" placeholder="密码"
|
||||||
|
name="Password" />
|
||||||
|
</div>
|
||||||
|
<button class="logmod-submit btn" type="submit"
|
||||||
|
value="Log in">登 录</button>
|
||||||
|
<div class="checkbox" id="logmod-password">
|
||||||
|
<label id="logmod-remember-password"><input type="checkbox" value="true"
|
||||||
|
name="RememberMe"><input type="hidden" value="false"
|
||||||
|
name="RememberMe">记住密码</label>
|
||||||
|
<div id="logmod-forget-password" class="pull-right"><a
|
||||||
|
href="/Account/ForgotPassword" class="forget-password">忘记密码</a></div>
|
||||||
|
</div>
|
||||||
|
<input name="__RequestVerificationToken" type="hidden"
|
||||||
|
value="CfDJ8MyNMqFNaC9JmJW13PvY-91NCe3U_rFij5nf6ni2NtxaxhQNSuonDbK7198YMfFdErEyhk-CHiByyDQaq371N3GUx0c8Xma0F0F2J2UQaszZgfjT5vxTV4O4viF6YoPDWMO2yLbeN7ok83_uz1DD-nU" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$.validator.addMethod("username", function (value, element) {
|
||||||
|
return this.optional(element) || /^[\u4e00-\u9fa5_a-zA-Z0-9_]{3,15}$/i.test(value);
|
||||||
|
}, "用户名只能使用中英文,数字和下划线,长度请控制在3-15字节以内");
|
||||||
|
|
||||||
|
$.validator.addMethod("usernameTaken", function (value, element) {
|
||||||
|
var valid = true;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: '/Account/VerifyUserName',
|
||||||
|
data: JSON.stringify(value),
|
||||||
|
async: false,
|
||||||
|
error: function (XMLHttpRequest, textStatus, errorThrown) {
|
||||||
|
//alert("Request: " + XMLHttpRequest.toString() + "\n\nStatus: " + textStatus + "\n\nError: " + errorThrown);
|
||||||
|
},
|
||||||
|
success: function (data) {
|
||||||
|
valid = data.nottaken;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}, "用户名已经被使用");
|
||||||
|
|
||||||
|
$("#registerForm").validate({
|
||||||
|
rules: {
|
||||||
|
UserName: {
|
||||||
|
required: true,
|
||||||
|
username: "用户名只能使用中英文,数字和下划线,长度请控制在3-15字节以内",
|
||||||
|
usernameTaken: "用户名已经被使用"
|
||||||
|
},
|
||||||
|
Password: {
|
||||||
|
required: true,
|
||||||
|
minlength: 6
|
||||||
|
},
|
||||||
|
ConfirmPassword: {
|
||||||
|
equalTo: "#register-password"
|
||||||
|
},
|
||||||
|
Email: {
|
||||||
|
required: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
UserName: {
|
||||||
|
required: "请输入用户名",
|
||||||
|
username: "用户名只能使用中英文,数字和下划线,长度请控制在3-15字节以内",
|
||||||
|
usernameTaken: "用户名已经被使用"
|
||||||
|
},
|
||||||
|
Password: {
|
||||||
|
required: "请输入密码",
|
||||||
|
minlength: "密码设置错误,密码长度必须大于6位"
|
||||||
|
},
|
||||||
|
ConfirmPassword: {
|
||||||
|
equalTo: "两次输入的密码不一致,请再输入一次您之前输入的密码"
|
||||||
|
},
|
||||||
|
Email: {
|
||||||
|
required: "请输入Email地址",
|
||||||
|
email: "提供的Email地址无效,请检查并重试"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#loginForm").validate({
|
||||||
|
rules: {
|
||||||
|
UserName: {
|
||||||
|
required: true,
|
||||||
|
username: "用户名只能使用中英文,数字和下划线,长度请控制在3-15字节以内"
|
||||||
|
},
|
||||||
|
Password: {
|
||||||
|
required: true,
|
||||||
|
minlength: 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
UserName: {
|
||||||
|
required: "请输入用户名",
|
||||||
|
username: "用户名只能使用中英文,数字和下划线,长度请控制在3-15字节以内"
|
||||||
|
},
|
||||||
|
Password: {
|
||||||
|
required: "请输入密码",
|
||||||
|
minlength: "密码设置错误,密码长度必须大于6位"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="margin: auto;width:100%;height:85vh;" class="hidden-lg hidden-md">
|
||||||
|
<div class="m-login">
|
||||||
|
<div class="m-tool-title" style="padding-top: 7rem; color:#555;">
|
||||||
|
登陆mikan账号
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;margin-top: 2rem;">
|
||||||
|
<img src="/images/mikan-pic.png" style="width: 6rem;">
|
||||||
|
</div>
|
||||||
|
<p style="color:red; margin-left: 2.1rem;" class="m-login-error">
|
||||||
|
登录失败,请重试.</p>
|
||||||
|
<form action="/Account/Login?ReturnUrl=%2FAccount%2FManage" id="mobileLoginForm" method="post">
|
||||||
|
<div id="mobileLoginInput">
|
||||||
|
<input type="text" class="form-control" aria-label="..." placeholder="用户名" name="UserName">
|
||||||
|
<input type="password" class="form-control" aria-label="..." placeholder="密码" name="Password">
|
||||||
|
</div>
|
||||||
|
<button class="form-control" type="submit">登录</button>
|
||||||
|
<input name="__RequestVerificationToken" type="hidden"
|
||||||
|
value="CfDJ8MyNMqFNaC9JmJW13PvY-91NCe3U_rFij5nf6ni2NtxaxhQNSuonDbK7198YMfFdErEyhk-CHiByyDQaq371N3GUx0c8Xma0F0F2J2UQaszZgfjT5vxTV4O4viF6YoPDWMO2yLbeN7ok83_uz1DD-nU" />
|
||||||
|
</form>
|
||||||
|
<div class="m-goto-registry">
|
||||||
|
<a href="/Account/Register" class="w-other-c" style="color:#3bc0c3">立即注册</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$("#mobileLoginForm").validate({
|
||||||
|
rules: {
|
||||||
|
UserName: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
Password: {
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
UserName: {
|
||||||
|
required: "用户名或密码错误",
|
||||||
|
},
|
||||||
|
Password: {
|
||||||
|
required: "用户名或密码错误",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
username: "UserName Password"
|
||||||
|
},
|
||||||
|
errorPlacement: function (error, element) {
|
||||||
|
error.insertAfter("#mobileLoginInput");
|
||||||
|
},
|
||||||
|
errorClass: "m-login-error"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script src="/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||||
|
<script src="/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
LoginModalController.initialize(1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="modal modal-fullscreen fade" id="modal-nav" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"
|
||||||
|
aria-hidden="true" style="background-color:#3bc0c3;">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body" style="margin: auto;width:100%;">
|
||||||
|
<div class="m-tool">
|
||||||
|
<span class="m-close clickable"><i class="fa fa-times" aria-hidden="true" data-toggle="modal"
|
||||||
|
data-target="#modal-nav"></i></span>
|
||||||
|
<div class="m-tool-toolbar">
|
||||||
|
<img src="/images/mikan-pic.png" style="width: 3rem;">
|
||||||
|
<img src="/images/mikan-text.png" style="width: 7rem;">
|
||||||
|
</div>
|
||||||
|
<div class="m-tool-list">
|
||||||
|
<ul>
|
||||||
|
<li><a href="/" class="link">主页</a></li>
|
||||||
|
<li class="m-tool-search-change"><a href="/Home/MyBangumi" class="link">订阅</a></li>
|
||||||
|
<li onclick="tool.clickSearch()" class="m-tool-search-change">
|
||||||
|
<i class="fa fa-search" aria-hidden="true"></i> 搜索站内
|
||||||
|
</li>
|
||||||
|
<li class="m-tool-search-input">
|
||||||
|
<form method="get" action="/Home/Search">
|
||||||
|
<div style="display: flex;height: 100%;">
|
||||||
|
<input type="text" class="form-control" name="searchstr"
|
||||||
|
style="font-size:16px;" />
|
||||||
|
<span style="width: 5rem;" onclick="tool.resetSearch()">取消</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal modal-fullscreen fade" id="modal-login" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"
|
||||||
|
aria-hidden="true" style="background-color:#edf1f2;">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body" style="margin: auto;width:100%;height:85vh;">
|
||||||
|
<div class="m-login">
|
||||||
|
<span class="m-left clickable"><i class="fa fa-angle-left" aria-hidden="true"
|
||||||
|
data-toggle="modal" data-target="#modal-login"></i></span>
|
||||||
|
|
||||||
|
<div class="m-tool-title">
|
||||||
|
登陆mikan账号
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;margin-top: 2rem;">
|
||||||
|
<img src="/images/mikan-pic.png" style="width: 6rem;">
|
||||||
|
</div>
|
||||||
|
<form action="/Account/Login?ReturnUrl=%2FAccount%2FLogin" method="post">
|
||||||
|
<div>
|
||||||
|
<input type="text" class="form-control" aria-label="..." placeholder="用户名"
|
||||||
|
name="UserName">
|
||||||
|
<input type="password" class="form-control" aria-label="..." placeholder="密码"
|
||||||
|
name="Password">
|
||||||
|
</div>
|
||||||
|
<button class="form-control" type="submit">登录</button>
|
||||||
|
<input name="__RequestVerificationToken" type="hidden"
|
||||||
|
value="CfDJ8MyNMqFNaC9JmJW13PvY-91NCe3U_rFij5nf6ni2NtxaxhQNSuonDbK7198YMfFdErEyhk-CHiByyDQaq371N3GUx0c8Xma0F0F2J2UQaszZgfjT5vxTV4O4viF6YoPDWMO2yLbeN7ok83_uz1DD-nU" />
|
||||||
|
</form>
|
||||||
|
<div class="m-goto-registry">
|
||||||
|
<a href="/Account/Register" class="w-other-c" style="color:#3bc0c3">立即注册</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="footer hidden-xs hidden-sm">
|
||||||
|
<div id="sk-footer" class="container text-center">
|
||||||
|
<div>Powered by Mikan Project <a href="/Home/Contact" target="_blank">联系我们</a></div>
|
||||||
|
<div>Cooperate by PlaymateCat@Lisa</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var tool = {};
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
var inputPEl = $('.m-tool-search-input');
|
||||||
|
var inputEl = inputPEl.find('input');
|
||||||
|
var changeEl = $('.m-tool-search-change');
|
||||||
|
inputPEl.hide();
|
||||||
|
tool.clickSearch = clickSearch;
|
||||||
|
tool.resetSearch = resetSearch;
|
||||||
|
|
||||||
|
function clickSearch() {
|
||||||
|
changeEl.hide();
|
||||||
|
inputPEl.show();
|
||||||
|
inputEl.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearch() {
|
||||||
|
changeEl.show();
|
||||||
|
inputPEl.hide();
|
||||||
|
inputEl.val('');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var pageUtil;
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
pageUtil = {
|
||||||
|
isMobile: isMobile
|
||||||
|
};
|
||||||
|
|
||||||
|
function isMobile() {
|
||||||
|
var check = false;
|
||||||
|
(function (a) {
|
||||||
|
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true;
|
||||||
|
})(navigator.userAgent || navigator.vendor || window.opera);
|
||||||
|
return check;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
//detect if page is mobile
|
||||||
|
if (pageUtil.isMobile()) {
|
||||||
|
document.getElementsByTagName('html')[0].style['font-size'] = window.innerWidth / 32 + 'px';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<!-- here put your own javascript -->
|
||||||
|
|
||||||
|
|
||||||
|
<script src="/js/mikan.min.js?v=7USd_hfRE7KH46vQBdF29boa3ENWKMVFRTyD9a8XEDg"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</html>
|
@ -32,3 +32,5 @@ http-cache = { version = "0.20", features = [
|
|||||||
"manager-moka",
|
"manager-moka",
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
reqwest_cookie_store = { version = "0.8.0", features = ["serde"] }
|
reqwest_cookie_store = { version = "0.8.0", features = ["serde"] }
|
||||||
|
|
||||||
|
util = { workspace = true }
|
||||||
|
@ -16,6 +16,7 @@ use reqwest_tracing::TracingMiddleware;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
|
use util::OptDynErr;
|
||||||
|
|
||||||
use crate::get_random_ua;
|
use crate::get_random_ua;
|
||||||
|
|
||||||
@ -111,6 +112,8 @@ pub enum HttpClientError {
|
|||||||
HttpError { source: http::Error },
|
HttpError { source: http::Error },
|
||||||
#[snafu(display("Failed to parse cookies: {}", source))]
|
#[snafu(display("Failed to parse cookies: {}", source))]
|
||||||
ParseCookiesError { source: serde_json::Error },
|
ParseCookiesError { source: serde_json::Error },
|
||||||
|
#[snafu(display("Failed to save cookies, message: {}, source: {:?}", message, source))]
|
||||||
|
SaveCookiesError { message: String, source: OptDynErr },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait HttpClientTrait: Deref<Target = ClientWithMiddleware> + Debug {}
|
pub trait HttpClientTrait: Deref<Target = ClientWithMiddleware> + Debug {}
|
||||||
@ -123,9 +126,13 @@ pub struct HttpClientFork {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HttpClientFork {
|
impl HttpClientFork {
|
||||||
pub fn attach_cookies(mut self, cookies: &str) -> Result<Self, HttpClientError> {
|
pub fn attach_cookies(mut self, cookies: Option<&str>) -> Result<Self, HttpClientError> {
|
||||||
let cookie_store: CookieStore = serde_json::from_str(cookies)
|
let cookie_store = if let Some(cookies) = cookies {
|
||||||
.map_err(|err| HttpClientError::ParseCookiesError { source: err })?;
|
serde_json::from_str(cookies)
|
||||||
|
.map_err(|err| HttpClientError::ParseCookiesError { source: err })?
|
||||||
|
} else {
|
||||||
|
CookieStore::default()
|
||||||
|
};
|
||||||
|
|
||||||
let cookies_store = Arc::new(CookieStoreRwLock::new(cookie_store));
|
let cookies_store = Arc::new(CookieStoreRwLock::new(cookie_store));
|
||||||
|
|
||||||
@ -329,6 +336,35 @@ impl HttpClient {
|
|||||||
cookie_store,
|
cookie_store,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_cookie_store_to_json(&self) -> Result<Option<String>, HttpClientError> {
|
||||||
|
if let Some(cookie_store) = self.cookie_store.as_ref() {
|
||||||
|
let json = {
|
||||||
|
let cookie_store =
|
||||||
|
cookie_store
|
||||||
|
.read()
|
||||||
|
.map_err(|_| HttpClientError::SaveCookiesError {
|
||||||
|
message: "Failed to read cookie store".to_string(),
|
||||||
|
source: None.into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if cookie_store.iter_any().next().is_none() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::to_string(&cookie_store as &CookieStore).map_err(|err| {
|
||||||
|
HttpClientError::SaveCookiesError {
|
||||||
|
message: "Failed to serialize cookie store".to_string(),
|
||||||
|
source: OptDynErr::some_boxed(err),
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(json))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HttpClient {
|
impl Default for HttpClient {
|
||||||
|
Loading…
Reference in New Issue
Block a user