├── .gitignore ├── Cargo.toml ├── bili_lib ├── Cargo.toml └── src │ └── lib.rs ├── README.md ├── bili_ticket ├── src │ ├── main.rs │ ├── task.rs │ └── app.rs └── Cargo.toml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /.idea 4 | /config.json 5 | /bili_ticket/src/test.rs -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "bili_ticket", 5 | "bili_lib", 6 | ] 7 | -------------------------------------------------------------------------------- /bili_lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bili_lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | reqwest = { version = "0.11", features = ["multipart", "json"] } 10 | serde_json = "1" 11 | serde = { version = "1.0", features = ["derive"] } 12 | 13 | [profile.release] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## B站会员购展出快速购票脚本 2 | - bili_lib: 封装b站相关的api 3 | - bili_ticket: 主程序,使用egui 4 | 5 | ## 更新日志 6 | - 2024.3.18 7 | 8 | 继续约会,再休息一天 9 | - 2024.3.17 10 | 11 | 和女朋友约会,休息一天 12 | - 2024.3.16 13 | 14 | 不同票型的表单生成功能、立即购票功能 15 | - 2024.3.15 16 | 17 | 支付功能、取消订单功能、更换账户功能、日期场次的显示和选择功能 18 | - 2024.3.14 19 | 20 | 上传github、基础gui布局、输出终端、账号登录功能、订单查询功能、票品查询和头图显示功能 21 | - 2024.3.13 22 | 23 | bili_lib基础api封装 -------------------------------------------------------------------------------- /bili_ticket/src/main.rs: -------------------------------------------------------------------------------- 1 | #![windows_subsystem = "windows"] 2 | use crate::app::BiliTicket; 3 | use eframe::Theme; 4 | 5 | mod app; 6 | mod task; 7 | #[cfg(test)] 8 | mod test; 9 | 10 | fn main() { 11 | let mut native_options = eframe::NativeOptions::default(); 12 | native_options.follow_system_theme = false; 13 | native_options.default_theme = Theme::Light; 14 | eframe::run_native( 15 | "Bili_Ticket", 16 | native_options, 17 | Box::new(|cc| Box::new(BiliTicket::new(cc))), 18 | ) 19 | .unwrap(); 20 | } 21 | -------------------------------------------------------------------------------- /bili_ticket/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bili_ticket" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | reqwest = { version = "0.11", features = ["json", "blocking"] } 10 | tokio = {version = "1.36", features = ["full"]} 11 | serde_json = "1" 12 | serde_urlencoded = "0.7" 13 | serde = { version = "1.0", features = ["derive"] } 14 | eframe = { version = "0.26"} 15 | #fast_qr = { version = "0.12", features = ["image"] } 16 | egui_extras = { version = "0.26", features = ["image", "http"] } 17 | image = { version = "0.24", features = ["png", "jpeg"] } # Add the types you want support for 18 | bili_lib = { path = "../bili_lib" } 19 | #regex = "1.10" 20 | 21 | 22 | [profile.release] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 秦诗染 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /bili_ticket/src/task.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{NamePhoneForm, BiliTicket, Config, OrderType}; 2 | use bili_lib::{ 3 | cancel_order, generate_qrcode, nav_info, order_create, order_list_shows, order_prepare, 4 | pay_param, project_info, qrcode_login, ClickPosition, CreateForm, PrepareForm, 5 | }; 6 | use reqwest::header::{HeaderMap, COOKIE}; 7 | use serde_json::Error; 8 | use std::sync::atomic::Ordering; 9 | use std::sync::Arc; 10 | use std::time::{Duration, SystemTime, UNIX_EPOCH}; 11 | use tokio::time::sleep; 12 | 13 | pub fn load_config() -> Config { 14 | Config::default() 15 | } 16 | 17 | impl BiliTicket { 18 | pub fn buy_ticket_now(&self, prepare_form: &PrepareForm) { 19 | match self.config.order_type { 20 | OrderType::Anonymous => { 21 | 22 | } 23 | OrderType::NamePhone => { 24 | self.name_phone_buy_now(&prepare_form, &self.config.name_phone_form); 25 | } 26 | OrderType::Deliver => {} 27 | OrderType::Buyer => {} 28 | 29 | 30 | 31 | } 32 | } 33 | 34 | fn name_phone_buy_now(&self, prepare_form: &PrepareForm, anonymous_form: &NamePhoneForm) { 35 | let token = self.prepare_order(prepare_form); 36 | //let regex = Regex::new(r"deviceFingerprint=;").unwrap(); 37 | //let cookie = self.config.cookie.lock().unwrap().to_string(); 38 | //let cap = regex.captures(cookie.as_str()).unwrap(); 39 | //let device_id = cap["device_id"].to_string(); 40 | let device_id = "".to_string(); 41 | let create_form = CreateForm { 42 | project_id: prepare_form.project_id, 43 | screen_id: prepare_form.screen_id, 44 | sku_id: prepare_form.sku_id, 45 | count: prepare_form.count, 46 | pay_money: self.config.ticket.price * prepare_form.count as u64, 47 | order_type: 1, 48 | timestamp: SystemTime::now() 49 | .duration_since(UNIX_EPOCH) 50 | .unwrap() 51 | .as_millis(), 52 | token, 53 | device_id, 54 | click_position: ClickPosition { 55 | x: 935, 56 | y: 786, 57 | origin: SystemTime::now() 58 | .duration_since(UNIX_EPOCH) 59 | .unwrap() 60 | .as_millis() 61 | - 1000, 62 | now: SystemTime::now() 63 | .duration_since(UNIX_EPOCH) 64 | .unwrap() 65 | .as_millis(), 66 | }, 67 | new_risk: false, 68 | request_source: "pc_new".to_string(), 69 | buyer: anonymous_form.name.clone(), 70 | tel: anonymous_form.phone.clone(), 71 | }; 72 | match self.runtime.block_on(order_create( 73 | &self.client, 74 | self.build_headers(), 75 | &create_form, 76 | )) { 77 | Ok(order_id) => { 78 | self.print_terminal("购票成功"); 79 | } 80 | Err(e) => self.print_terminal(format!("购票失败,错误信息: {}\n", e).as_str()), 81 | } 82 | } 83 | 84 | pub fn prepare_order(&self, prepare_form: &PrepareForm) -> String { 85 | let token = self.runtime.block_on(order_prepare( 86 | &self.client, 87 | self.build_headers(), 88 | prepare_form, 89 | )); 90 | token.unwrap() 91 | } 92 | 93 | pub fn cancel_order(&self, order_id: &String) { 94 | match self 95 | .runtime 96 | .block_on(cancel_order(&self.client, self.build_headers(), order_id)) 97 | { 98 | Ok(_) => { 99 | self.print_terminal("取消订单成功!\n"); 100 | } 101 | Err(_) => { 102 | self.print_terminal("取消订单失败,可能是订单不存在?\n"); 103 | } 104 | }; 105 | } 106 | 107 | pub fn print_terminal(&self, str: &str) { 108 | let tb = Arc::clone(&self.terminal_buffer); 109 | if !tb.lock().unwrap().ends_with("\n") { 110 | tb.lock().unwrap().push('\n'); 111 | } 112 | tb.lock().unwrap().push_str(str); 113 | } 114 | 115 | pub fn do_paying(&mut self, order_id: String) -> bool { 116 | match self 117 | .runtime 118 | .block_on(pay_param(&self.client, self.build_headers(), &order_id)) 119 | { 120 | Ok(url) => { 121 | self.config.pay_code = format!( 122 | "https://api.pwmqr.com/qrcode/create/?url={}", 123 | url.replace("&", "%26") 124 | ); 125 | true 126 | } 127 | Err(_) => { 128 | self.print_terminal("请求支付码失败,可能是订单不存在?\n"); 129 | false 130 | } 131 | } 132 | } 133 | pub fn do_login(&mut self) { 134 | let (url, qrcode_key) = self.runtime.block_on(generate_qrcode(&self.client)); 135 | // let qrcode = QRBuilder::new(url).build().unwrap(); 136 | // self.login_qr = ImageBuilder::default() 137 | // .shape(Shape::RoundedSquare) 138 | // .background_color([255, 255, 255, 0]) 139 | // .fit_width(250) 140 | // .to_bytes(&qrcode) 141 | // .unwrap(); 142 | self.login_qr_url = format!( 143 | "https://api.pwmqr.com/qrcode/create/?url={}", 144 | url.replace("&", "%26") 145 | ); 146 | self.print_terminal("请扫描二维码登录:\n"); 147 | self.show_login_qr = true; 148 | let logging = Arc::clone(&self.logging); 149 | let tb = Arc::clone(&self.terminal_buffer); 150 | let c = Arc::clone(&self.config.cookie); 151 | let cl = Arc::clone(&self.client); 152 | let is_l = Arc::clone(&self.config.is_login); 153 | self.runtime.spawn(async move { 154 | loop { 155 | sleep(Duration::from_secs(3)).await; 156 | let (code, msg, cookie) = qrcode_login(&cl, &qrcode_key).await; 157 | match code { 158 | 0 => { 159 | *c.lock().unwrap() = cookie.unwrap(); 160 | is_l.store(true, Ordering::Relaxed); 161 | tb.lock().unwrap().push_str("登录成功!\n"); 162 | logging.store(false, Ordering::Relaxed); 163 | break; 164 | } 165 | _ => { 166 | continue; 167 | } 168 | } 169 | } 170 | }); 171 | } 172 | 173 | pub fn handler_orders(&self) { 174 | let cl = Arc::clone(&self.client); 175 | let orders = Arc::clone(&self.config.orders); 176 | let headers = self.build_headers(); 177 | let is_handler = Arc::clone(&self.handler_order); 178 | self.runtime.spawn(async move { 179 | loop { 180 | if !is_handler.load(Ordering::Relaxed) { 181 | return; 182 | } 183 | let res = order_list_shows(&cl, headers.clone()).await; 184 | *orders.lock().unwrap() = res; 185 | sleep(Duration::from_millis(1500)).await; 186 | } 187 | }); 188 | } 189 | 190 | pub fn get_user_head(&mut self) { 191 | let (uname, face_img) = self 192 | .runtime 193 | .block_on(nav_info(&self.client, self.build_headers())); 194 | self.config.user_name = uname; 195 | self.config.user_head_img_url = face_img; 196 | } 197 | 198 | pub fn get_project(&mut self) -> Result<(), Error> { 199 | let project = self.runtime.block_on(project_info( 200 | &self.client, 201 | self.config.target_project.parse().unwrap(), 202 | ))?; 203 | self.config.project = Option::from(project.clone()); 204 | if project.screen_list[0].ticket_list[0].anonymous_buy { 205 | self.config.order_type = OrderType::Anonymous 206 | } else if project.screen_list[0].delivery_type == 3 { 207 | self.config.order_type = OrderType::Deliver; 208 | } else if project.buyer_info == "2,1" { 209 | self.config.order_type = OrderType::Buyer; 210 | } else if project.need_contact == 1 { 211 | self.config.order_type = OrderType::NamePhone; 212 | } 213 | Ok(()) 214 | } 215 | 216 | fn build_headers(&self) -> HeaderMap { 217 | let mut headers = HeaderMap::new(); 218 | headers.insert(COOKIE, self.config.cookie.lock().unwrap().parse().unwrap()); 219 | headers 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /bili_lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::HeaderMap; 2 | use reqwest::Client; 3 | use serde::ser::SerializeStruct; 4 | use serde::{Deserialize, Serialize, Serializer}; 5 | use serde_json::Error; 6 | use std::string::ToString; 7 | use std::time::{SystemTime, UNIX_EPOCH}; 8 | 9 | #[derive(Serialize, Deserialize, Clone)] 10 | pub struct Buyer { 11 | id: i64, //购票人id 12 | uid: i64, //b站id 13 | account_channel: String, //未知 14 | personal_id: String, //身份证号 15 | name: String, //真实姓名, 16 | id_card_front: String, 17 | id_card_back: String, 18 | is_default: i8, //是否为默认账户 19 | tel: String, //手机号码 20 | error_code: i64, //错误码,无错误则为0 21 | id_type: i64, //未知,为0 22 | verify_status: i64, //未知,为1 23 | #[serde(rename = "accountId")] 24 | account_id: i64, //同uid 25 | } 26 | #[derive(Serialize, Deserialize, Clone)] 27 | pub struct ItemInfo { 28 | pub name: String, 29 | img: String, 30 | screen_id: i32, 31 | screen_name: String, 32 | express_fee: i32, 33 | express_free_flag: i32, 34 | deliver_type: i32, 35 | screen_type: i32, 36 | //project_ver_id: i64, 37 | link_id: i32, 38 | ticket_type: i32, 39 | time: i32, 40 | ticket_type_name: String, 41 | } 42 | #[derive(Serialize, Deserialize, Clone)] 43 | pub struct Img { 44 | url: String, 45 | desc: String, 46 | } 47 | #[derive(Serialize, Deserialize, Clone)] 48 | pub struct Order { 49 | pub order_id: String, 50 | uid: String, 51 | order_type: i32, 52 | item_id: i32, 53 | #[serde(rename = "item_info")] 54 | pub item_info: ItemInfo, 55 | count: i32, 56 | total_money: i32, 57 | pay_money: i32, 58 | express_fee: i32, 59 | pay_channel: i32, 60 | status: i32, 61 | sub_status: i32, 62 | refund_status: i32, 63 | pay_time: i32, 64 | ctime: String, 65 | source: String, 66 | ticket_agent: String, 67 | img: Img, 68 | current_time: i32, 69 | deliver_type_name: String, 70 | free_deliver: bool, 71 | create_at: i32, 72 | pay_remain_time: i32, 73 | pub sub_status_name: String, 74 | } 75 | 76 | #[derive(Serialize, Deserialize, Clone)] 77 | pub struct Project { 78 | pub buyer_info: String, //“2,1”为实名认证 79 | pub need_contact: i32, //需要联系人表单吗 80 | pub name: String, 81 | status: i32, 82 | is_sale: i32, 83 | start_time: u64, 84 | end_time: u64, 85 | sale_begin: i64, 86 | sale_end: u64, 87 | sale_start: u64, 88 | pub performance_image: String, 89 | pub screen_list: Vec, 90 | } 91 | #[derive(Serialize, Deserialize, Clone)] 92 | pub struct Screen { 93 | pub id: i64, 94 | pub delivery_type: i32, //配送方式,1为电子票,3为纸质票 95 | start_time: u64, 96 | pub name: String, 97 | #[serde(rename = "type")] 98 | type_: i32, 99 | ticket_type: i32, 100 | screen_type: i32, 101 | pub ticket_list: Vec, 102 | } 103 | #[derive(Serialize, Deserialize, Clone, Default)] 104 | pub struct Ticket { 105 | pub id: i64, 106 | pub anonymous_buy: bool, //匿名购买 107 | pub price: u64, 108 | pub desc: String, 109 | sale_start: String, 110 | sale_end: String, 111 | sale_type: i32, 112 | pub is_sale: i32, 113 | screen_name: String, 114 | #[serde(rename = "clickable")] 115 | click_able: bool, 116 | } 117 | #[derive(Serialize, Clone, Default)] 118 | pub struct PrepareForm { 119 | pub project_id: i64, 120 | pub screen_id: i64, 121 | pub sku_id: i64, 122 | pub order_type: i32, 123 | pub count: u8, 124 | } 125 | 126 | pub struct CreateForm { 127 | pub project_id: i64, 128 | pub screen_id: i64, 129 | pub sku_id: i64, 130 | pub count: u8, 131 | pub pay_money: u64, 132 | pub order_type: i32, 133 | pub timestamp: u128, 134 | pub token: String, 135 | //#[serde(rename = "deviceId")] 136 | pub device_id: String, 137 | //#[serde(rename = "clickPosition")] 138 | pub click_position: ClickPosition, 139 | //#[serde(rename = "new_risk")] 140 | pub new_risk: bool, 141 | //#[serde(rename = "requestSource")] 142 | pub request_source: String, //电脑为pc-new 143 | pub buyer: String, //联系人姓名 144 | pub tel: String, //联系人电话 145 | } 146 | 147 | #[derive(Serialize)] 148 | pub struct ClickPosition { 149 | pub x: u32, 150 | pub y: u32, 151 | pub origin: u128, //点击按钮时候的时间戳 152 | pub now: u128, //发送请求时候的时间戳 153 | } 154 | 155 | impl Serialize for CreateForm { 156 | fn serialize(&self, serializer: S) -> Result 157 | where 158 | S: Serializer, 159 | { 160 | let mut state = serializer.serialize_struct("createForm", 14)?; 161 | state.serialize_field("project_id", &self.project_id)?; 162 | state.serialize_field("screen_id", &self.screen_id)?; 163 | state.serialize_field("sku_id", &self.sku_id)?; 164 | state.serialize_field("count", &self.count)?; 165 | state.serialize_field("pay_money", &self.pay_money)?; 166 | state.serialize_field("order_type", &self.order_type)?; 167 | state.serialize_field("timestamp", &self.timestamp)?; 168 | state.serialize_field("token", &self.token)?; 169 | state.serialize_field("deviceId", &self.device_id)?; 170 | state.serialize_field( 171 | "clickPosition", 172 | &serde_json::to_string(&self.click_position).unwrap(), 173 | )?; 174 | state.serialize_field("newRisk", &self.new_risk)?; 175 | state.serialize_field("requestSource", &self.request_source)?; 176 | state.serialize_field("buyer", &self.buyer)?; 177 | state.serialize_field("tel", &self.tel)?; 178 | state.end() 179 | } 180 | } 181 | 182 | pub async fn cancel_order( 183 | client: &Client, 184 | headers: HeaderMap, 185 | order_id: &String, 186 | ) -> Result<(), ()> { 187 | let res = client 188 | .get("https://show.bilibili.com/api/ticket/order/cancel?order_id=".to_string() + order_id) 189 | .headers(headers) 190 | .send() 191 | .await 192 | .unwrap(); 193 | let json = res.json::().await.unwrap(); 194 | let errno = json.get("errno").unwrap(); 195 | if errno.as_i64().unwrap() == 0 { 196 | Ok(()) 197 | } else { 198 | Err(()) 199 | } 200 | } 201 | 202 | pub async fn pay_param( 203 | client: &Client, 204 | headers: HeaderMap, 205 | order_id: &String, 206 | ) -> Result { 207 | let res = client 208 | .get( 209 | "https://show.bilibili.com/api/ticket/order/getPayParam?order_id=".to_string() 210 | + order_id, 211 | ) 212 | .headers(headers) 213 | .send() 214 | .await 215 | .unwrap(); 216 | let json = res.json::().await.unwrap(); 217 | let data = json.get("data").unwrap(); 218 | if let Some(url) = data.get("code_url") { 219 | return Ok(url.as_str().unwrap().to_string()); 220 | } else { 221 | return Err(()); 222 | } 223 | } 224 | 225 | pub async fn order_info(client: &Client, headers: HeaderMap, order_id: String) -> Order { 226 | let timestamp = SystemTime::now() 227 | .duration_since(UNIX_EPOCH) 228 | .unwrap() 229 | .as_millis(); 230 | let res = client 231 | .get(format!( 232 | "https://show.bilibili.com/api/ticket/order/info?order_id={}×tamp={}", 233 | order_id, timestamp 234 | )) 235 | .headers(headers) 236 | .send() 237 | .await 238 | .unwrap(); 239 | res.json::().await.unwrap() 240 | } 241 | 242 | pub async fn order_prepare( 243 | client: &Client, 244 | headers: HeaderMap, 245 | prepare_form: &PrepareForm, 246 | ) -> Result { 247 | let res = client 248 | .post("https://show.bilibili.com/api/ticket/order/prepare") 249 | .headers(headers) 250 | .form(prepare_form) 251 | .send() 252 | .await 253 | .unwrap(); 254 | let json = res.json::().await.unwrap(); 255 | let data = json.get("data").unwrap(); 256 | let token = data.get("token").unwrap().as_str().unwrap().to_string(); 257 | Ok(token) 258 | } 259 | 260 | pub async fn order_create( 261 | client: &Client, 262 | headers: HeaderMap, 263 | create_form: &CreateForm, 264 | ) -> Result { 265 | let res = client 266 | .post("https://show.bilibili.com/api/ticket/order/createV2") 267 | .headers(headers) 268 | .form(create_form) 269 | .send() 270 | .await 271 | .unwrap(); 272 | let json = res.json::().await.unwrap(); 273 | let data = json.get("data").unwrap(); 274 | if let Some(order_id) = data.get("orderId") { 275 | Ok(order_id.as_u64().unwrap()) 276 | } else { 277 | Err(json.get("msg").unwrap().as_str().unwrap().to_string()) 278 | } 279 | } 280 | 281 | pub async fn nav_info(client: &Client, headers: HeaderMap) -> (String, String) { 282 | let res = client 283 | .get("https://api.bilibili.com/x/web-interface/nav") 284 | .headers(headers) 285 | .send() 286 | .await 287 | .unwrap(); 288 | let json = res.json::().await.unwrap(); 289 | let data = json.get("data").unwrap(); 290 | ( 291 | data.get("uname").unwrap().as_str().unwrap().to_string(), 292 | data.get("face").unwrap().as_str().unwrap().to_string(), 293 | ) 294 | } 295 | 296 | pub async fn order_list_shows(client: &Client, headers: HeaderMap) -> Vec { 297 | let res = client 298 | .get("https://show.bilibili.com/api/ticket/order/list?page=0&page_size=20") 299 | .headers(headers.clone()) 300 | .send() 301 | .await 302 | .unwrap(); 303 | let json = res.json::().await.unwrap(); 304 | let data = json.get("data").unwrap(); 305 | let order_list: Vec = serde_json::from_value(data.get("list").unwrap().clone()).unwrap(); 306 | order_list 307 | } 308 | 309 | pub async fn buyer_info(client: Client, headers: HeaderMap) -> Vec { 310 | let res = client 311 | .get("https://show.bilibili.com/api/ticket/buyer/list") 312 | .headers(headers) 313 | .send() 314 | .await 315 | .unwrap(); 316 | let json = res.json::().await.unwrap(); 317 | let data = json.get("data").unwrap(); 318 | let buyer_list: Vec = serde_json::from_value(data.get("list").unwrap().clone()).unwrap(); 319 | buyer_list 320 | } 321 | pub async fn generate_qrcode(client: &Client) -> (String, String) { 322 | let res = client 323 | .get("https://passport.bilibili.com/x/passport-login/web/qrcode/generate") 324 | .send() 325 | .await 326 | .unwrap(); 327 | let json = res.json::().await.unwrap(); 328 | let data = json.get("data").unwrap(); 329 | ( 330 | data.get("url").unwrap().as_str().unwrap().to_string(), 331 | data.get("qrcode_key") 332 | .unwrap() 333 | .as_str() 334 | .unwrap() 335 | .to_string(), 336 | ) 337 | } 338 | 339 | pub async fn qrcode_login(client: &Client, qrcode_key: &String) -> (i64, String, Option) { 340 | let res = client 341 | .get( 342 | "https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=" 343 | .to_string() 344 | + qrcode_key, 345 | ) 346 | .send() 347 | .await 348 | .unwrap(); 349 | let head = res.headers().clone(); 350 | let json = res.json::().await.unwrap(); 351 | let data = json.get("data").unwrap(); 352 | let re_cookie: Option; 353 | if let Some(cookie) = head.get("Set-Cookie") { 354 | re_cookie = Option::from(cookie.to_str().unwrap().to_string()); 355 | } else { 356 | re_cookie = None; 357 | } 358 | 359 | ( 360 | data.get("code").unwrap().as_i64().unwrap(), 361 | data.get("message").unwrap().as_str().unwrap().to_string(), 362 | re_cookie, 363 | ) 364 | } 365 | 366 | pub async fn project_info(client: &Client, project_id: u64) -> Result { 367 | let res = client 368 | .get( 369 | "https://show.bilibili.com/api/ticket/project/get?id=".to_string() 370 | + &project_id.to_string(), 371 | ) 372 | .send() 373 | .await 374 | .unwrap(); 375 | let json = res.json::().await.unwrap(); 376 | let data = json.get("data").unwrap(); 377 | let mut project: Project = serde_json::from_value(data.clone())?; 378 | let performance_image: serde_json::Value = 379 | serde_json::from_str(&project.performance_image).unwrap(); 380 | let performance_image_url = "http:".to_string() 381 | + performance_image 382 | .get("first") 383 | .unwrap() 384 | .get("url") 385 | .unwrap() 386 | .as_str() 387 | .unwrap(); 388 | project.performance_image = performance_image_url; 389 | 390 | Ok(project) 391 | } -------------------------------------------------------------------------------- /bili_ticket/src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::task::load_config; 2 | use bili_lib::{Order, PrepareForm, Project, Ticket}; 3 | use eframe::egui::{vec2, FontData, FontFamily, Image, Vec2}; 4 | use eframe::{egui, App, CreationContext}; 5 | use egui_extras::install_image_loaders; 6 | use reqwest::Client; 7 | use serde::{Deserialize, Serialize}; 8 | use std::fs; 9 | use std::fs::File; 10 | use std::io::{Read, Write}; 11 | use std::sync::atomic::{AtomicBool, Ordering}; 12 | use std::sync::{Arc, Mutex}; 13 | 14 | #[derive(Serialize, Deserialize, Clone)] 15 | pub enum OrderType { 16 | Anonymous, 17 | NamePhone, 18 | Deliver, 19 | Buyer, 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Clone, Default)] 23 | pub struct NamePhoneForm { 24 | pub name: String, 25 | pub phone: String, 26 | } 27 | 28 | pub struct BiliTicket { 29 | pub runtime: tokio::runtime::Runtime, 30 | pub terminal_buffer: Arc>, 31 | pub show_login_qr: bool, 32 | pub login_qr_url: String, 33 | pub config: Config, 34 | pub logging: Arc, 35 | pub client: Arc, 36 | pub blocking_client: Arc, 37 | pub handler_order: Arc, 38 | } 39 | 40 | #[derive(Serialize, Deserialize, Clone)] 41 | pub struct Config { 42 | ticket_count: String, 43 | pub name_phone_form: NamePhoneForm, 44 | loaded_user_head: bool, 45 | pub order_type: OrderType, 46 | select_order_id: String, 47 | is_select_ticket: bool, 48 | pub ticket: Ticket, 49 | screen_id: i64, 50 | is_got_project: bool, 51 | project_image_url: String, 52 | pub show_paying_qr: bool, 53 | pub project: Option, 54 | pub target_project: String, 55 | pub user_name: String, 56 | pub user_head_img_url: String, 57 | pub orders: Arc>>, 58 | pub cookie: Arc>, 59 | pub is_login: Arc, 60 | pub pay_code: String, 61 | } 62 | 63 | impl Default for Config { 64 | fn default() -> Self { 65 | Config { 66 | ticket_count: String::from("1"), 67 | name_phone_form: NamePhoneForm::default(), 68 | loaded_user_head: false, 69 | order_type: OrderType::Anonymous, 70 | select_order_id: String::default(), 71 | is_select_ticket: false, 72 | ticket: Ticket::default(), 73 | screen_id: 0, 74 | is_got_project: false, 75 | project_image_url: String::default(), 76 | project: None, 77 | target_project: String::default(), 78 | user_name: String::default(), 79 | user_head_img_url: String::default(), 80 | orders: Arc::new(Mutex::new(vec![])), 81 | cookie: Arc::new(Mutex::new(String::default())), 82 | is_login: Arc::new(AtomicBool::new(false)), 83 | pay_code: String::default(), 84 | show_paying_qr: false, 85 | } 86 | } 87 | } 88 | 89 | impl Default for BiliTicket { 90 | fn default() -> Self { 91 | BiliTicket { 92 | handler_order: Arc::new(AtomicBool::new(false)), 93 | blocking_client: Arc::new(reqwest::blocking::Client::new()), 94 | client: Arc::new(Client::new()), 95 | runtime: tokio::runtime::Builder::new_multi_thread() 96 | .enable_all() 97 | .build() 98 | .unwrap(), 99 | terminal_buffer: Arc::new(Mutex::new(String::default())), 100 | show_login_qr: false, 101 | login_qr_url: String::default(), 102 | config: load_config(), 103 | logging: Arc::new(AtomicBool::new(false)), 104 | } 105 | } 106 | } 107 | 108 | impl BiliTicket { 109 | pub fn new(cc: &CreationContext<'_>) -> Self { 110 | install_image_loaders(&cc.egui_ctx); 111 | 112 | let mut fonts = eframe::egui::FontDefinitions::default(); 113 | if let Ok(buf) = fs::read("C:\\Windows\\Fonts\\msyh.ttc") { 114 | fonts 115 | .font_data 116 | .insert("微软雅黑".to_owned(), FontData::from_owned(buf)); 117 | fonts 118 | .families 119 | .insert(FontFamily::Monospace, vec!["微软雅黑".to_owned()]); 120 | fonts 121 | .families 122 | .insert(FontFamily::Proportional, vec!["微软雅黑".to_owned()]); 123 | } else { 124 | println!("Failed to load font 微软雅黑"); 125 | } 126 | cc.egui_ctx.set_fonts(fonts); 127 | let mut bili_ticket = Self::default(); 128 | 129 | bili_ticket.first_loading(); 130 | 131 | bili_ticket 132 | } 133 | 134 | fn first_loading(&mut self) { 135 | if let Ok(mut f) = File::open("./config.json") { 136 | if let Ok(config) = serde_json::from_reader(f) { 137 | self.config = config; 138 | } 139 | } 140 | } 141 | 142 | fn ui_menu(&mut self, ctx: &egui::Context) { 143 | egui::TopBottomPanel::top("menu panel") 144 | .resizable(true) 145 | .show(ctx, |ui| { 146 | egui::menu::bar(ui, |ui| { 147 | ui.menu_button("账户", |ui| { 148 | if ui.button("更换账户").clicked() { 149 | self.config = Config::default(); 150 | self.handler_order.store(false, Ordering::Relaxed); 151 | } 152 | }); 153 | }); 154 | }); 155 | } 156 | fn ui_ticket(&mut self, ctx: &egui::Context) { 157 | egui::CentralPanel::default().show(ctx, |ui| { 158 | ui.horizontal(|ui| { 159 | ui.vertical(|ui| { 160 | if self.config.is_login.load(Ordering::Relaxed) == true { 161 | ui.horizontal_wrapped(|ui| { 162 | ui.label("请输入票品id"); 163 | ui.text_edit_singleline(&mut self.config.target_project); 164 | if ui.button("确认").clicked() { 165 | self.config.is_select_ticket = false; 166 | self.print_terminal("加载票品信息...\n"); 167 | let mut flag = true; 168 | self.get_project().unwrap_or_else(|_| { 169 | self.print_terminal("载入票品信息失败,可能是票品不存在\n"); 170 | flag = false; 171 | }); 172 | 173 | if flag { 174 | ctx.forget_image(&self.config.project_image_url); 175 | self.config.project_image_url = 176 | self.config.project.clone().unwrap().performance_image; 177 | self.config.is_got_project = true; 178 | self.print_terminal("载入商品信息完成\n"); 179 | } 180 | } 181 | }); 182 | } 183 | if self.config.is_got_project { 184 | ui.add( 185 | Image::from_uri(self.config.project_image_url.clone()) 186 | .fit_to_exact_size(Vec2::new(405.0, 720.0)), 187 | ); 188 | } 189 | }); 190 | ui.vertical(|ui| { 191 | if self.config.is_got_project { 192 | let mut ticket_list: Vec = vec![]; 193 | ui.horizontal(|ui| { 194 | for screen in self.config.project.clone().unwrap().screen_list { 195 | let mut but = egui::Button::new(screen.name.clone()); 196 | if self.config.screen_id == screen.id { 197 | but = but.selected(true); 198 | } 199 | 200 | if ui.add(but).clicked() { 201 | self.config.screen_id = screen.id; 202 | } 203 | 204 | if screen.id == self.config.screen_id { 205 | ticket_list = screen.ticket_list; 206 | } 207 | } 208 | }); 209 | ui.horizontal(|ui| { 210 | for ticket in ticket_list { 211 | let mut but = egui::Button::new(ticket.desc.clone()); 212 | if self.config.ticket.id == ticket.id { 213 | but = but.selected(true); 214 | } 215 | if ui.add(but).clicked() { 216 | self.config.ticket = ticket; 217 | self.config.is_select_ticket = true; 218 | } 219 | } 220 | }); 221 | 222 | if self.config.is_select_ticket { 223 | match self.config.order_type { 224 | OrderType::NamePhone => { 225 | ui.horizontal(|ui| { 226 | ui.vertical(|ui| { 227 | ui.label("姓名"); 228 | ui.text_edit_singleline( 229 | &mut self.config.name_phone_form.name, 230 | ); 231 | }); 232 | ui.vertical(|ui| { 233 | ui.label("手机号"); 234 | ui.text_edit_singleline( 235 | &mut self.config.name_phone_form.phone, 236 | ); 237 | }); 238 | }); 239 | ui.horizontal(|ui| { 240 | ui.label("购买数量"); 241 | if ui.button("-").clicked() { 242 | self.config.ticket_count = 243 | (self.config.ticket_count.parse::().unwrap() 244 | - 1) 245 | .to_string(); 246 | } 247 | ui.add_sized( 248 | vec2(100.0, 20.0), 249 | egui::TextEdit::singleline( 250 | &mut self.config.ticket_count, 251 | ), 252 | ); 253 | if ui.button("+").clicked() { 254 | self.config.ticket_count = 255 | (self.config.ticket_count.parse::().unwrap() 256 | + 1) 257 | .to_string(); 258 | } 259 | }); 260 | ui.horizontal(|ui| { 261 | if ui.button("立即购票").clicked() { 262 | if self.config.ticket_count.parse().unwrap() == 0 { 263 | self.print_terminal("购买数量不能为0\n"); 264 | } else { 265 | let prepare_form = PrepareForm { 266 | project_id: self 267 | .config 268 | .target_project 269 | .parse() 270 | .unwrap(), 271 | screen_id: self.config.screen_id, 272 | order_type: 1, 273 | count: self.config.ticket_count.parse().unwrap(), 274 | sku_id: self.config.ticket.id, 275 | }; 276 | self.buy_ticket_now( 277 | &prepare_form, 278 | &self.config.name_phone_form, 279 | ); 280 | } 281 | } 282 | }); 283 | } 284 | OrderType::Buyer => {} 285 | OrderType::Deliver => {} 286 | OrderType::Anonymous => {} 287 | } 288 | } 289 | } 290 | }); 291 | }); 292 | }); 293 | } 294 | fn ui_order(&mut self, ctx: &egui::Context) { 295 | egui::TopBottomPanel::bottom("order panel") 296 | .resizable(true) 297 | .default_height(100.0) 298 | .show(ctx, |ui| { 299 | if self.config.is_login.load(Ordering::Relaxed) == false { 300 | if self.logging.load(Ordering::Relaxed) == false { 301 | self.do_login(); 302 | self.logging.store(true, Ordering::Relaxed); 303 | } 304 | ctx.request_repaint(); 305 | ui.add(Image::from_uri(self.login_qr_url.clone())); 306 | } 307 | if self.config.is_login.load(Ordering::Relaxed) == true { 308 | ctx.forget_image(&self.login_qr_url); 309 | if !self.config.loaded_user_head { 310 | self.print_terminal("加载用户昵称和头像...\n"); 311 | self.get_user_head(); 312 | self.config.loaded_user_head = true; 313 | } 314 | if !self.handler_order.load(Ordering::Relaxed) { 315 | self.print_terminal("加载订单数据...\n"); 316 | self.handler_orders(); 317 | self.handler_order.store(true, Ordering::Relaxed); 318 | } 319 | 320 | egui::SidePanel::left("user_head panel").show_inside(ui, |ui| { 321 | ui.heading(self.config.user_name.clone()); 322 | ui.add(Image::from_uri(self.config.user_head_img_url.clone())); 323 | }); 324 | let height = ui.available_size().y; 325 | egui::ScrollArea::vertical() 326 | .auto_shrink(false) 327 | .show(ui, |ui| { 328 | ui.horizontal(|ui| { 329 | ui.vertical(|ui| { 330 | let orders = self.config.orders.lock().unwrap().clone(); 331 | let mut no_pay_wait = true; 332 | for order in orders { 333 | ui.horizontal_wrapped(|ui| { 334 | ui.label(order.item_info.name.clone()); 335 | ui.label(order.sub_status_name.clone()); 336 | if order.sub_status_name.clone() == "待支付" { 337 | no_pay_wait = false; 338 | if self.config.select_order_id != order.order_id { 339 | if ui.link("点此显示付款二维码").clicked() 340 | { 341 | ctx.forget_image(&self.config.pay_code); 342 | self.print_terminal("请求付款二维码...\n"); 343 | if self.do_paying(order.order_id.clone()) { 344 | self.config.show_paying_qr = true; 345 | self.config.select_order_id = 346 | order.order_id.clone(); 347 | } 348 | } 349 | } else { 350 | if ui.link("隐藏付款码").clicked() { 351 | ctx.forget_image(&self.config.pay_code); 352 | self.print_terminal("删除缓存\n"); 353 | self.config.show_paying_qr = false; 354 | self.config.pay_code = String::default(); 355 | self.config.select_order_id = 356 | String::default(); 357 | } 358 | } 359 | 360 | if ui.link("取消订单").clicked() { 361 | self.cancel_order(&order.order_id); 362 | } 363 | } 364 | }); 365 | } 366 | if no_pay_wait { 367 | self.config.show_paying_qr = false; 368 | self.config.pay_code = String::default(); 369 | self.config.select_order_id = String::default(); 370 | } 371 | }); 372 | ui.vertical(|ui| { 373 | if self.config.show_paying_qr { 374 | ui.add_sized( 375 | vec2(height, height), 376 | Image::from_uri(&self.config.pay_code), 377 | ); 378 | } 379 | }); 380 | }); 381 | }); 382 | } 383 | }); 384 | } 385 | fn ui_terminal(&self, ctx: &egui::Context) { 386 | egui::TopBottomPanel::bottom("terminal panel") 387 | .resizable(true) 388 | .default_height(100.0) 389 | .show(ctx, |ui| { 390 | egui::ScrollArea::vertical() 391 | .stick_to_bottom(true) 392 | .show(ui, |ui| { 393 | ui.add_sized( 394 | ui.available_size(), 395 | egui::TextEdit::multiline( 396 | &mut self.terminal_buffer.lock().unwrap().as_str(), 397 | ), 398 | ); 399 | }); 400 | }); 401 | } 402 | fn ui_argument(&self, ctx: &egui::Context) { 403 | egui::SidePanel::right("argument panel") 404 | .resizable(true) 405 | .show(ctx, |ui| {}); 406 | } 407 | } 408 | 409 | impl App for BiliTicket { 410 | fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { 411 | self.ui_menu(ctx); 412 | self.ui_ticket(ctx); 413 | self.ui_terminal(ctx); 414 | self.ui_argument(ctx); 415 | self.ui_order(ctx); 416 | if ctx.input(|i| i.viewport().close_requested()) { 417 | let mut file = File::create("./config.json").unwrap(); 418 | let json = serde_json::to_string(&self.config).unwrap(); 419 | file.write_all(json.as_ref()).unwrap(); 420 | } 421 | } 422 | } --------------------------------------------------------------------------------