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",
|
||||
"snafu",
|
||||
"url",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2,7 +2,9 @@ use std::{fmt::Debug, ops::Deref, sync::Arc};
|
||||
|
||||
use fetch::{HttpClient, HttpClientTrait};
|
||||
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 util::OptDynErr;
|
||||
|
||||
@ -137,7 +139,7 @@ impl MikanClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_credential(
|
||||
pub async fn submit_credential_form(
|
||||
&self,
|
||||
ctx: Arc<dyn AppContextTrait>,
|
||||
subscriber_id: i32,
|
||||
@ -159,49 +161,67 @@ impl MikanClient {
|
||||
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(
|
||||
&self,
|
||||
ctx: Arc<dyn AppContextTrait>,
|
||||
credential_id: Option<i32>,
|
||||
credential_id: i32,
|
||||
) -> RecorderResult<Self> {
|
||||
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?;
|
||||
if let Some(credential) = credential {
|
||||
if credential.credential_type != Credential3rdType::Mikan {
|
||||
return Err(RecorderError::Credential3rdError {
|
||||
message: "credential is not a mikan credential".to_string(),
|
||||
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")),
|
||||
));
|
||||
let credential = credential_3rd::Model::find_by_id(ctx.clone(), credential_id).await?;
|
||||
if let Some(credential) = credential {
|
||||
if credential.credential_type != Credential3rdType::Mikan {
|
||||
return Err(RecorderError::Credential3rdError {
|
||||
message: "credential is not a mikan credential".to_string(),
|
||||
source: None.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
let userpass_credential: UserPassCredential =
|
||||
credential.try_into_userpass_credential(ctx)?;
|
||||
|
||||
fork = fork.attach_cookies(userpass_credential.cookies.as_deref())?;
|
||||
|
||||
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 {
|
||||
|
@ -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_ID: &str = "202";
|
||||
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_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 config::MikanConfig;
|
||||
pub use constants::{
|
||||
MIKAN_ACCOUNT_MANAGE_PAGE_PATH, MIKAN_LOGIN_PAGE_PATH, MIKAN_LOGIN_PAGE_SEARCH,
|
||||
MIKAN_POSTER_BUCKET_KEY, MIKAN_UNKNOWN_FANSUB_ID, MIKAN_UNKNOWN_FANSUB_NAME,
|
||||
MIKAN_ACCOUNT_MANAGE_PAGE_PATH, MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH,
|
||||
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 rss::{
|
||||
|
@ -10,7 +10,8 @@ use tracing::instrument;
|
||||
use url::Url;
|
||||
|
||||
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,
|
||||
};
|
||||
use crate::{
|
||||
@ -183,7 +184,7 @@ pub fn build_mikan_season_flow_url(
|
||||
season_str: MikanSeasonStr,
|
||||
) -> Url {
|
||||
let mut url = mikan_base_url;
|
||||
url.set_path("/Home/BangumiCoverFlow");
|
||||
url.set_path(MIKAN_SEASON_FLOW_PAGE_PATH);
|
||||
url.query_pairs_mut()
|
||||
.append_pair("year", &year.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,
|
||||
) -> Url {
|
||||
let mut url = mikan_base_url;
|
||||
url.set_path("/ExpandBangumi");
|
||||
url.set_path(MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH);
|
||||
url.query_pairs_mut()
|
||||
.append_pair("bangumiId", mikan_bangumi_id)
|
||||
.append_pair("showSubscribed", "true");
|
||||
@ -651,7 +652,7 @@ pub async fn scrape_mikan_bangumi_meta_list_from_season_flow_url(
|
||||
credential_id: i32,
|
||||
) -> RecorderResult<Vec<MikanBangumiMeta>> {
|
||||
let mikan_client = mikan_client
|
||||
.fork_with_credential(ctx.clone(), Some(credential_id))
|
||||
.fork_with_credential(ctx.clone(), credential_id)
|
||||
.await?;
|
||||
|
||||
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![];
|
||||
|
||||
mikan_client
|
||||
.sync_credential_cookies(ctx.clone(), credential_id)
|
||||
.await?;
|
||||
|
||||
for bangumi_index in bangumi_indices_meta {
|
||||
let bangumi_title = bangumi_index.bangumi_title.clone();
|
||||
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);
|
||||
}
|
||||
|
||||
mikan_client
|
||||
.sync_credential_cookies(ctx, credential_id)
|
||||
.await?;
|
||||
|
||||
Ok(bangumi_metas)
|
||||
}
|
||||
|
||||
@ -704,7 +713,6 @@ mod test {
|
||||
#![allow(unused_variables)]
|
||||
use std::fs;
|
||||
|
||||
use fetch::get_random_ua;
|
||||
use rstest::{fixture, rstest};
|
||||
use tracing::Level;
|
||||
use url::Url;
|
||||
@ -712,11 +720,16 @@ mod test {
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
extract::mikan::MikanCredentialForm,
|
||||
extract::mikan::{MIKAN_BANGUMI_EXPAND_SUBSCRIBED_PAGE_PATH, MIKAN_SEASON_FLOW_PAGE_PATH},
|
||||
test_utils::{
|
||||
app::UnitTestAppContext, crypto::build_testing_crypto_service,
|
||||
database::build_testing_database_service, mikan::build_testing_mikan_client,
|
||||
storage::build_testing_storage_service, tracing::try_init_testing_tracing,
|
||||
app::UnitTestAppContext,
|
||||
crypto::build_testing_crypto_service,
|
||||
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(
|
||||
before_each: (),
|
||||
) -> RecorderResult<()> {
|
||||
let mut mikan_server = mockito::Server::new_async().await;
|
||||
let mikan_base_url = Url::parse(&mikan_server.url())?;
|
||||
let mut mikan_server = MikanMockServer::new().await?;
|
||||
let mikan_base_url = mikan_server.base_url().clone();
|
||||
|
||||
let app_ctx = {
|
||||
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 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
|
||||
.save_credential(
|
||||
app_ctx.clone(),
|
||||
1,
|
||||
MikanCredentialForm {
|
||||
username: String::from("test_username"),
|
||||
password: String::from("test_password"),
|
||||
user_agent: get_random_ua().to_string(),
|
||||
},
|
||||
)
|
||||
.submit_credential_form(app_ctx.clone(), 1, build_testing_mikan_credential_form())
|
||||
.await?;
|
||||
|
||||
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(
|
||||
mikan_client,
|
||||
@ -975,6 +1018,26 @@ mod test {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -987,8 +1050,9 @@ mod test {
|
||||
let mikan_base_url = Url::parse(&mikan_server.url())?;
|
||||
let mikan_client = build_testing_mikan_client(mikan_base_url.clone()).await?;
|
||||
|
||||
let episode_homepage_url =
|
||||
mikan_base_url.join("/Home/Episode/475184dce83ea2b82902592a5ac3343f6d54b36a")?;
|
||||
let episode_homepage_url = mikan_base_url
|
||||
.clone()
|
||||
.join("/Home/Episode/475184dce83ea2b82902592a5ac3343f6d54b36a")?;
|
||||
|
||||
let episode_homepage_mock = mikan_server
|
||||
.mock("GET", episode_homepage_url.path())
|
||||
@ -1058,101 +1122,4 @@ mod test {
|
||||
|
||||
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::{
|
||||
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(
|
||||
base_mikan_url: impl IntoUrl,
|
||||
) -> RecorderResult<MikanClient> {
|
||||
@ -17,3 +29,145 @@ pub async fn build_testing_mikan_client(
|
||||
.await?;
|
||||
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",
|
||||
], default-features = false }
|
||||
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_with::serde_as;
|
||||
use snafu::Snafu;
|
||||
use util::OptDynErr;
|
||||
|
||||
use crate::get_random_ua;
|
||||
|
||||
@ -111,6 +112,8 @@ pub enum HttpClientError {
|
||||
HttpError { source: http::Error },
|
||||
#[snafu(display("Failed to parse cookies: {}", source))]
|
||||
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 {}
|
||||
@ -123,9 +126,13 @@ pub struct HttpClientFork {
|
||||
}
|
||||
|
||||
impl HttpClientFork {
|
||||
pub fn attach_cookies(mut self, cookies: &str) -> Result<Self, HttpClientError> {
|
||||
let cookie_store: CookieStore = serde_json::from_str(cookies)
|
||||
.map_err(|err| HttpClientError::ParseCookiesError { source: err })?;
|
||||
pub fn attach_cookies(mut self, cookies: Option<&str>) -> Result<Self, HttpClientError> {
|
||||
let cookie_store = if let Some(cookies) = cookies {
|
||||
serde_json::from_str(cookies)
|
||||
.map_err(|err| HttpClientError::ParseCookiesError { source: err })?
|
||||
} else {
|
||||
CookieStore::default()
|
||||
};
|
||||
|
||||
let cookies_store = Arc::new(CookieStoreRwLock::new(cookie_store));
|
||||
|
||||
@ -329,6 +336,35 @@ impl HttpClient {
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user