From c0c8af9094585684e2891abf0d304d34cb2c26c7 Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Thu, 4 Sep 2025 04:28:04 +0800 Subject: [PATCH 01/11] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=A4=84=E7=90=86=E4=BB=BB=E5=8A=A1=E4=B8=ADffmpeg?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E7=9A=84=E6=9E=84=E5=BB=BA=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../downloader/tasks/video_process_task.rs | 64 +++++++------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/src-tauri/src/downloader/tasks/video_process_task.rs b/src-tauri/src/downloader/tasks/video_process_task.rs index 97edf6f..e1a2e8f 100644 --- a/src-tauri/src/downloader/tasks/video_process_task.rs +++ b/src-tauri/src/downloader/tasks/video_process_task.rs @@ -98,27 +98,16 @@ impl VideoProcessTask { tokio::spawn(async move { let mut command = std::process::Command::new(ffmpeg_program); - command - .arg("-i") - .arg(video_path_clone) - .arg("-i") - .arg(audio_path_clone); - + command.arg("-i").arg(video_path_clone); + command.arg("-i").arg(audio_path_clone); if let Some(metadata_path) = metadata_path_clone { - command - .arg("-i") - .arg(metadata_path) - .arg("-map_metadata") - .arg("2"); + command.arg("-i").arg(metadata_path); + command.arg("-map_metadata").arg("2"); } - command - .arg("-c") - .arg("copy") - .arg("-map") - .arg("0:v:0") - .arg("-map") - .arg("1:a:0"); + command.arg("-c").arg("copy"); + command.arg("-map").arg("0:v:0"); + command.arg("-map").arg("1:a:0"); command.arg(output_path_clone).arg("-y"); @@ -198,19 +187,14 @@ impl VideoProcessTask { tauri::async_runtime::spawn_blocking(move || { let mut command = std::process::Command::new(ffmpeg_program); - command - .arg("-i") - .arg(video_path_clone) - .arg("-i") - .arg(audio_path_clone) - .arg("-c") - .arg("copy") - .arg("-map") - .arg("0:v:0") - .arg("-map") - .arg("1:a:0") - .arg(output_path_clone) - .arg("-y"); + command.arg("-i").arg(video_path_clone); + command.arg("-i").arg(audio_path_clone); + + command.arg("-c").arg("copy"); + command.arg("-map").arg("0:v:0"); + command.arg("-map").arg("1:a:0"); + + command.arg(output_path_clone).arg("-y"); #[cfg(target_os = "windows")] { @@ -286,17 +270,13 @@ impl VideoProcessTask { tauri::async_runtime::spawn_blocking(move || { let mut command = std::process::Command::new(ffmpeg_program); - command - .arg("-i") - .arg(video_path_clone) - .arg("-i") - .arg(metadata_path_clone) - .arg("-map_metadata") - .arg("1") - .arg("-c") - .arg("copy") - .arg(output_path_clone) - .arg("-y"); + command.arg("-i").arg(video_path_clone); + command.arg("-i").arg(metadata_path_clone); + + command.arg("-map_metadata").arg("1"); + command.arg("-c").arg("copy"); + + command.arg(output_path_clone).arg("-y"); #[cfg(target_os = "windows")] { From ae73e474c73958ef3bd1e202dfb4d09f3f2f1603 Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Thu, 4 Sep 2025 04:28:19 +0800 Subject: [PATCH 02/11] =?UTF-8?q?style:=20=E5=BE=AE=E8=B0=83=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E8=BF=9B=E5=BA=A6=E7=BB=84=E4=BB=B6=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E9=A2=9C=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DownloadProgress.vue | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/panes/DownloadPane/components/DownloadProgress.vue b/src/panes/DownloadPane/components/DownloadProgress.vue index 44a9c6c..c7ad5ea 100644 --- a/src/panes/DownloadPane/components/DownloadProgress.vue +++ b/src/panes/DownloadPane/components/DownloadProgress.vue @@ -153,7 +153,7 @@ function handleSearchClick() { P{{ p.part_order }} {{ p.part_title }} -
+
视频(编码:{{ p.video_task.codec_type }} 画质:{{ p.video_task.video_quality }}) @@ -161,19 +161,19 @@ function handleSearchClick() { 音频(音质:{{ p.audio_task.audio_quality }}) - 自动合并 - 标记章节 - 标记广告 + 自动合并 + 标记章节 + 标记广告 - xml弹幕 - ass弹幕 - json弹幕 + xml弹幕 + ass弹幕 + json弹幕 - 字幕 - 封面 + 字幕 + 封面 - nfo刮削 - json刮削 + nfo刮削 + json刮削
From 5ed41ef7b65e2b08bf8411cb67d631a4ec5edd6a Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Fri, 5 Sep 2025 05:03:52 +0800 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=20=E7=94=BB=E8=B4=A8=E3=80=81=E9=9F=B3=E8=B4=A8?= =?UTF-8?q?=E3=80=81=E7=BC=96=E7=A0=81=20=E7=9A=84=E4=BC=98=E5=85=88?= =?UTF-8?q?=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- pnpm-lock.yaml | 21 +++ src-tauri/src/config.rs | 137 ++++----------- src-tauri/src/downloader/tasks/audio_task.rs | 84 +++------- src-tauri/src/downloader/tasks/video_task.rs | 82 +++++---- src-tauri/src/types/audio_quality.rs | 2 + src-tauri/src/types/codec_type.rs | 3 + src-tauri/src/types/video_quality.rs | 2 + src/bindings.ts | 5 +- .../components/DownloadSettings.vue | 157 +++++++++++------- 10 files changed, 219 insertions(+), 277 deletions(-) diff --git a/package.json b/package.json index cbe2930..34a9da0 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "pinia": "^3.0.3", "unplugin-auto-import": "^19.3.0", "unplugin-vue-components": "^28.8.0", - "vue": "^3.5.13" + "vue": "^3.5.13", + "vue-draggable-plus": "^0.6.0" }, "devDependencies": { "@eslint/js": "^9.30.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 043e0e4..e3222ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: vue: specifier: ^3.5.13 version: 3.5.17(typescript@5.6.3) + vue-draggable-plus: + specifier: ^0.6.0 + version: 0.6.0(@types/sortablejs@1.15.8) devDependencies: '@eslint/js': specifier: ^9.30.1 @@ -738,6 +741,9 @@ packages: '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/sortablejs@1.15.8': + resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} + '@typescript-eslint/eslint-plugin@8.36.0': resolution: {integrity: sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1963,6 +1969,15 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-draggable-plus@0.6.0: + resolution: {integrity: sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==} + peerDependencies: + '@types/sortablejs': ^1.15.0 + '@vue/composition-api': '*' + peerDependenciesMeta: + '@vue/composition-api': + optional: true + vue-eslint-parser@10.2.0: resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2576,6 +2591,8 @@ snapshots: '@types/lodash@4.17.20': {} + '@types/sortablejs@1.15.8': {} + '@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.6.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3966,6 +3983,10 @@ snapshots: vscode-uri@3.1.0: {} + vue-draggable-plus@0.6.0(@types/sortablejs@1.15.8): + dependencies: + '@types/sortablejs': 1.15.8 + vue-eslint-parser@10.2.0(eslint@9.30.1(jiti@2.4.2)): dependencies: debug: 4.4.1 diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 779c0ae..cad3aa7 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,11 +1,13 @@ use std::path::{Path, PathBuf}; -use num_enum::{FromPrimitive, IntoPrimitive}; use serde::{Deserialize, Serialize}; use specta::Type; use tauri::{AppHandle, Manager}; -use crate::danmaku_xml_to_ass::canvas::CanvasConfig; +use crate::{ + danmaku_xml_to_ass::canvas::CanvasConfig, + types::{audio_quality::AudioQuality, codec_type::CodecType, video_quality::VideoQuality}, +}; #[derive(Debug, Clone, Serialize, Deserialize, Type)] #[allow(clippy::struct_excessive_bools)] @@ -14,9 +16,9 @@ pub struct Config { pub download_dir: PathBuf, pub enable_file_logger: bool, pub sessdata: String, - pub prefer_video_quality: PreferVideoQuality, - pub prefer_codec_type: PreferCodecType, - pub prefer_audio_quality: PreferAudioQuality, + pub video_quality_priority: Vec, + pub codec_type_priority: Vec, + pub audio_quality_priority: Vec, pub download_video: bool, pub download_audio: bool, pub auto_merge: bool, @@ -96,13 +98,36 @@ impl Config { fn default(app_data_dir: &Path) -> Config { const DEFAULT_FMT_FOR_PART: &str = "{collection_title}/{episode_title}/{episode_title}-P{part_order} {part_title}"; + let default_video_quality_priority = vec![ + VideoQuality::Video8K, + VideoQuality::VideoDolby, + VideoQuality::VideoHDR, + VideoQuality::Video4K, + VideoQuality::Video1080P60, + VideoQuality::Video1080PPlus, + VideoQuality::Video1080P, + VideoQuality::VideoAiRepair, + VideoQuality::Video720P60, + VideoQuality::Video720P, + VideoQuality::Video480P, + VideoQuality::Video360P, + VideoQuality::Video240P, + ]; + let default_audio_quality_priority = vec![ + AudioQuality::AudioHiRes, + AudioQuality::AudioDolby, + AudioQuality::Audio192K, + AudioQuality::Audio132K, + AudioQuality::Audio64K, + ]; + Config { download_dir: app_data_dir.join("视频下载"), enable_file_logger: true, sessdata: String::new(), - prefer_video_quality: PreferVideoQuality::Best, - prefer_codec_type: PreferCodecType::AVC, - prefer_audio_quality: PreferAudioQuality::Best, + video_quality_priority: default_video_quality_priority, + codec_type_priority: vec![CodecType::AVC, CodecType::HEVC, CodecType::AV1], + audio_quality_priority: default_audio_quality_priority, download_video: true, download_audio: true, auto_merge: true, @@ -130,102 +155,6 @@ impl Config { } } -#[derive( - Debug, - Default, - Clone, - Copy, - PartialEq, - Serialize, - Deserialize, - Type, - IntoPrimitive, - FromPrimitive, -)] -#[repr(i64)] -pub enum PreferVideoQuality { - #[default] - Best = -1, - - #[serde(rename = "240P")] - Video240P = 6, - #[serde(rename = "360P")] - Video360P = 16, - #[serde(rename = "480P")] - Video480P = 32, - #[serde(rename = "720P")] - Video720P = 64, - #[serde(rename = "720P60")] - Video720P60 = 74, - #[serde(rename = "1080P")] - Video1080P = 80, - #[serde(rename = "AiRepair")] - VideoAiRepair = 100, - #[serde(rename = "1080P+")] - Video1080PPlus = 112, - #[serde(rename = "1080P60")] - Video1080P60 = 116, - #[serde(rename = "4K")] - Video4K = 120, - #[serde(rename = "HDR")] - VideoHDR = 125, - #[serde(rename = "Dolby")] - VideoDolby = 126, - #[serde(rename = "8K")] - Video8K = 127, -} - -#[derive( - Debug, - Default, - Clone, - Copy, - PartialEq, - Serialize, - Deserialize, - Type, - IntoPrimitive, - FromPrimitive, -)] -#[repr(i64)] -#[allow(clippy::upper_case_acronyms)] -pub enum PreferCodecType { - #[default] - Unknown = -1, - - AVC = 7, - HEVC = 12, - AV1 = 13, -} - -#[derive( - Debug, - Default, - Clone, - Copy, - PartialEq, - Serialize, - Deserialize, - Type, - IntoPrimitive, - FromPrimitive, -)] -#[repr(i64)] -pub enum PreferAudioQuality { - #[default] - Best = -1, - #[serde(rename = "64K")] - Audio64K = 30216, - #[serde(rename = "132K")] - Audio132K = 30232, - #[serde(rename = "192K")] - Audio192K = 30280, - #[serde(rename = "Dolby")] - AudioDolby = 30250, - #[serde(rename = "HiRes")] - AudioHiRes = 30251, -} - #[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Type)] pub enum ProxyMode { #[default] diff --git a/src-tauri/src/downloader/tasks/audio_task.rs b/src-tauri/src/downloader/tasks/audio_task.rs index 5cdd4fc..6593acc 100644 --- a/src-tauri/src/downloader/tasks/audio_task.rs +++ b/src-tauri/src/downloader/tasks/audio_task.rs @@ -1,5 +1,5 @@ use std::{ - cmp::Reverse, + collections::HashMap, fs::{File, OpenOptions}, sync::Arc, }; @@ -114,11 +114,7 @@ impl AudioTask { } } - if medias.is_empty() { - return Err(anyhow!("获取音频地址失败")); - } - - self.prepare(app, medias); + self.prepare(app, medias)?; Ok(()) } @@ -174,11 +170,7 @@ impl AudioTask { } } - if medias.is_empty() { - return Err(anyhow!("获取音频地址失败")); - } - - self.prepare(app, medias); + self.prepare(app, medias)?; Ok(()) } @@ -234,37 +226,28 @@ impl AudioTask { } } - if medias.is_empty() { - return Err(anyhow!("获取音频地址失败")); - } - - self.prepare(app, medias); + self.prepare(app, medias)?; Ok(()) } - fn prepare(&mut self, app: &AppHandle, mut medias: Vec) { - medias.sort_by_key(|m| Reverse(m.id.to_audio_quality_for_prepare())); - let best_quality_id = medias[0].id; + fn prepare(&mut self, app: &AppHandle, mut medias: Vec) -> anyhow::Result<()> { + if medias.is_empty() { + return Err(anyhow!("获取音频地址失败")); + } - let prefer_quality = app.get_config().read().prefer_audio_quality; - let prefer_quality_id: i64 = prefer_quality.into(); - let prefer_quality_found = medias.iter().any(|m| m.id == prefer_quality_id); - let quality_filtered_medias: Vec = if prefer_quality_found { - // 如果用户指定质量存在,则使用用户指定的质量 - medias - .into_iter() - .filter(|m| m.id == prefer_quality_id) - .collect() - } else { - // 否则使用最高质量 - medias - .into_iter() - .filter(|m| m.id == best_quality_id) - .collect() - }; + let quality_priority = app.get_config().read().audio_quality_priority.clone(); + let priority_map: HashMap<&AudioQuality, usize> = quality_priority + .iter() + .enumerate() + .map(|(index, quality)| (quality, index)) + .collect(); + medias.sort_by_key(|media| { + let quality: AudioQuality = media.id.into(); + priority_map.get(&quality).unwrap_or(&usize::MAX) + }); - let media = &quality_filtered_medias[0]; + let media = &medias[0]; self.audio_quality = media.id.into(); @@ -295,6 +278,8 @@ impl AudioTask { self.content_length = content_length; self.chunks = chunks; } + + Ok(()) } pub fn mark_uncompleted(&mut self) { @@ -427,30 +412,3 @@ struct MediaForPrepare { pub id: i64, pub url_with_content_length: Vec<(String, u64)>, } - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum AudioQualityForPrepare { - Audio64K, - Audio132K, - Audio192K, - AudioDolby, - AudioHiRes, -} - -trait ToAudioQualityForPrepare { - fn to_audio_quality_for_prepare(self) -> Option; -} - -impl ToAudioQualityForPrepare for i64 { - fn to_audio_quality_for_prepare(self) -> Option { - let audio_quality: AudioQuality = self.into(); - match audio_quality { - AudioQuality::Audio64K => Some(AudioQualityForPrepare::Audio64K), - AudioQuality::Audio132K => Some(AudioQualityForPrepare::Audio132K), - AudioQuality::Audio192K => Some(AudioQualityForPrepare::Audio192K), - AudioQuality::AudioDolby => Some(AudioQualityForPrepare::AudioDolby), - AudioQuality::AudioHiRes => Some(AudioQualityForPrepare::AudioHiRes), - AudioQuality::Unknown => None, - } - } -} diff --git a/src-tauri/src/downloader/tasks/video_task.rs b/src-tauri/src/downloader/tasks/video_task.rs index 80cd81d..6bb7421 100644 --- a/src-tauri/src/downloader/tasks/video_task.rs +++ b/src-tauri/src/downloader/tasks/video_task.rs @@ -1,5 +1,5 @@ use std::{ - cmp::Reverse, + collections::HashMap, fs::{File, OpenOptions}, sync::Arc, }; @@ -74,11 +74,7 @@ impl VideoTask { } } - if medias.is_empty() { - return Err(anyhow!("获取视频地址失败")); - } - - self.prepare(app, medias); + self.prepare(app, medias)?; Ok(()) } @@ -144,11 +140,7 @@ impl VideoTask { } } - if medias.is_empty() { - return Err(anyhow!("获取视频地址失败")); - } - - self.prepare(app, medias); + self.prepare(app, medias)?; Ok(()) } @@ -214,47 +206,49 @@ impl VideoTask { } } - if medias.is_empty() { - return Err(anyhow!("获取视频地址失败")); - } - - self.prepare(app, medias); + self.prepare(app, medias)?; Ok(()) } - fn prepare(&mut self, app: &AppHandle, mut medias: Vec) { - medias.sort_by_key(|m| Reverse(m.id)); - let best_quality_id = medias[0].id; + fn prepare(&mut self, app: &AppHandle, mut medias: Vec) -> anyhow::Result<()> { + if medias.is_empty() { + return Err(anyhow!("获取音频地址失败")); + } - let (prefer_quality, prefer_codec_type) = { + let (video_quality_priority, codec_type_priority) = { let config = app.get_config().inner().read(); - (config.prefer_video_quality, config.prefer_codec_type) + ( + config.video_quality_priority.clone(), + config.codec_type_priority.clone(), + ) }; - let prefer_quality_id: i64 = prefer_quality.into(); - let prefer_codec_id: i64 = prefer_codec_type.into(); - let prefer_quality_found = medias.iter().any(|m| m.id == prefer_quality_id); - let mut quality_filtered_medias: Vec = if prefer_quality_found { - // 如果用户指定质量存在,则使用用户指定的质量 - medias - .into_iter() - .filter(|m| m.id == prefer_quality_id) - .collect() - } else { - // 否则使用最高质量 - medias - .into_iter() - .filter(|m| m.id == best_quality_id) - .collect() - }; - // 按照 AVC > HEVC > AV1 的顺序排列 - quality_filtered_medias.sort_by_key(|m| m.codecid); - - let media = quality_filtered_medias + let video_priority_map: HashMap<&VideoQuality, usize> = video_quality_priority .iter() - .find(|m| m.codecid == prefer_codec_id) - .unwrap_or(&quality_filtered_medias[0]); + .enumerate() + .map(|(index, quality)| (quality, index)) + .collect(); + medias.sort_by_key(|media| { + let quality: VideoQuality = media.id.into(); + video_priority_map.get(&quality).unwrap_or(&usize::MAX) + }); + + let retain_id = medias[0].id; + medias.retain(|m| m.id == retain_id); + + let codec_priority_map: HashMap<&CodecType, usize> = codec_type_priority + .iter() + .enumerate() + .map(|(index, codec_type)| (codec_type, index)) + .collect(); + + medias.sort_by_key(|m| { + let codec_type: CodecType = m.codecid.into(); + codec_priority_map.get(&codec_type).unwrap_or(&usize::MAX) + }); + + let media = &medias[0]; self.video_quality = media.id.into(); self.codec_type = media.codecid.into(); @@ -286,6 +280,8 @@ impl VideoTask { self.content_length = content_length; self.chunks = chunks; } + + Ok(()) } pub fn mark_uncompleted(&mut self) { diff --git a/src-tauri/src/types/audio_quality.rs b/src-tauri/src/types/audio_quality.rs index 2743e10..09c0af6 100644 --- a/src-tauri/src/types/audio_quality.rs +++ b/src-tauri/src/types/audio_quality.rs @@ -7,6 +7,8 @@ use specta::Type; Debug, Clone, Copy, + Hash, + Eq, PartialEq, Serialize, Deserialize, diff --git a/src-tauri/src/types/codec_type.rs b/src-tauri/src/types/codec_type.rs index c9adaee..767149a 100644 --- a/src-tauri/src/types/codec_type.rs +++ b/src-tauri/src/types/codec_type.rs @@ -7,6 +7,8 @@ use specta::Type; Debug, Clone, Copy, + Hash, + Eq, PartialEq, Serialize, Deserialize, @@ -19,6 +21,7 @@ use specta::Type; pub enum CodecType { #[default] Unknown = -1, + Audio = 0, AVC = 7, HEVC = 12, diff --git a/src-tauri/src/types/video_quality.rs b/src-tauri/src/types/video_quality.rs index e224a91..a3a6e0a 100644 --- a/src-tauri/src/types/video_quality.rs +++ b/src-tauri/src/types/video_quality.rs @@ -7,6 +7,8 @@ use specta::Type; Debug, Clone, Copy, + Hash, + Eq, PartialEq, Serialize, Deserialize, diff --git a/src/bindings.ts b/src/bindings.ts index 0a65c08..11d5586 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -247,7 +247,7 @@ export type CntInfo = { collect: number; play: number; thumb_up: number; share: export type CntInfoInMedia = { collect: number; play: number; danmaku: number; vt: number; play_switch: number; reply: number; view_text_1: string } export type CodecType = "Unknown" | "Audio" | "AVC" | "HEVC" | "AV1" export type CommandError = { err_title: string; err_message: string } -export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; prefer_video_quality: PreferVideoQuality; prefer_codec_type: PreferCodecType; prefer_audio_quality: PreferAudioQuality; download_video: boolean; download_audio: boolean; auto_merge: boolean; embed_chapter: boolean; embed_skip: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; download_cover: boolean; download_nfo: boolean; download_json: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; proxy_mode: ProxyMode; proxy_host: string; proxy_port: number; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig } +export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; video_quality_priority: VideoQuality[]; codec_type_priority: CodecType[]; audio_quality_priority: AudioQuality[]; download_video: boolean; download_audio: boolean; auto_merge: boolean; embed_chapter: boolean; embed_skip: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; download_cover: boolean; download_nfo: boolean; download_json: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; proxy_mode: ProxyMode; proxy_host: string; proxy_port: number; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig } export type Consulting = { consulting_flag: boolean; consulting_url: string } export type ContentAttr = { text: string; bg_color: string; bg_color_night: string; img: string; multi_img: MultiImg } export type ContentList = { bold: boolean; content: string; number: string } @@ -331,9 +331,6 @@ export type PendantInCheese = { image: string; name: string; pid: number } export type PendantInUserInfo = { pid: number; name: string; image: string; expire: number; image_enhance: string; image_enhance_frame: string; n_pid: number } export type PlayStrategy = { strategies: string[] } export type Positive = { id: number; title: string } -export type PreferAudioQuality = "Best" | "64K" | "132K" | "192K" | "Dolby" | "HiRes" -export type PreferCodecType = "Unknown" | "AVC" | "HEVC" | "AV1" -export type PreferVideoQuality = "Best" | "240P" | "360P" | "480P" | "720P" | "720P60" | "1080P" | "AiRepair" | "1080P+" | "1080P60" | "4K" | "HDR" | "Dolby" | "8K" export type PreviewedPurchaseNote = { long_watch_text: string; pay_text: string; price_format: string; watch_text: string; watching_text: string } export type Producer = { mid: number; type: number; is_contribute: number | null; title: string } export type ProxyMode = "NoProxy" | "System" | "Custom" diff --git a/src/dialogs/SettingsDialog/components/DownloadSettings.vue b/src/dialogs/SettingsDialog/components/DownloadSettings.vue index 361a03c..3f533e8 100644 --- a/src/dialogs/SettingsDialog/components/DownloadSettings.vue +++ b/src/dialogs/SettingsDialog/components/DownloadSettings.vue @@ -1,34 +1,40 @@ + + From dd86eb9e410989c22cf1a15af09b9bfd9d450864 Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Sat, 6 Sep 2025 05:16:09 +0800 Subject: [PATCH 04/11] =?UTF-8?q?perf:=20=E4=BB=85=E5=9C=A8=E5=BF=85?= =?UTF-8?q?=E8=A6=81=E6=97=B6=E6=9E=84=E9=80=A0=E4=B8=8B=E8=BD=BD=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=9A=84Future?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/downloader/download_task.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/downloader/download_task.rs b/src-tauri/src/downloader/download_task.rs index f1e3cd9..658b446 100644 --- a/src-tauri/src/downloader/download_task.rs +++ b/src-tauri/src/downloader/download_task.rs @@ -170,7 +170,7 @@ impl DownloadTask { let download_task = async { download_task_option - .get_or_insert(Box::pin(self.download())) + .get_or_insert_with(|| Box::pin(self.download())) .await; }; From 605c55fdec6c240f1d5a93e26b7a9a5ca0bc6705 Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Sun, 7 Sep 2025 06:12:38 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=E5=90=8E=E7=AB=AF=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=8E=B7=E5=8F=96=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/bili_client.rs | 59 +++++++++++++++++-- src-tauri/src/commands.rs | 16 +++++ src-tauri/src/lib.rs | 1 + .../src/types/get_history_info_params.rs | 38 ++++++++++++ src-tauri/src/types/history_info.rs | 52 ++++++++++++++++ src-tauri/src/types/mod.rs | 2 + src/bindings.ts | 14 +++++ 9 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 src-tauri/src/types/get_history_info_params.rs create mode 100644 src-tauri/src/types/history_info.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1db3dec..b1947c4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -304,6 +304,7 @@ dependencies = [ "reqwest-retry", "serde", "serde_json", + "serde_repr", "specta", "specta-typescript", "strfmt", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7e7aaaa..10ceb50 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,6 +25,7 @@ tauri-plugin-dialog = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_repr = "0.1" specta = { version = "=2.0.0-rc.20", features = ["serde_json"] } tauri-specta = { version = "=2.0.0-rc.20", features = ["derive", "typescript"] } diff --git a/src-tauri/src/bili_client.rs b/src-tauri/src/bili_client.rs index 89f4c1d..8554b6b 100644 --- a/src-tauri/src/bili_client.rs +++ b/src-tauri/src/bili_client.rs @@ -26,11 +26,13 @@ use crate::{ cheese_media_url::CheeseMediaUrl, fav_folders::FavFolders, fav_info::FavInfo, get_bangumi_follow_info_params::GetBangumiFollowInfoParams, get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams, - get_fav_info_params::GetFavInfoParams, get_normal_info_params::GetNormalInfoParams, - get_user_video_info_params::GetUserVideoInfoParams, normal_info::NormalInfo, - normal_media_url::NormalMediaUrl, player_info::PlayerInfo, qrcode_data::QrcodeData, - qrcode_status::QrcodeStatus, skip_segments::SkipSegments, subtitle::Subtitle, tags::Tags, - user_info::UserInfo, user_video_info::UserVideoInfo, watch_later_info::WatchLaterInfo, + get_fav_info_params::GetFavInfoParams, get_history_info_params::GetHistoryInfoParams, + get_normal_info_params::GetNormalInfoParams, + get_user_video_info_params::GetUserVideoInfoParams, history_info::HistoryInfo, + normal_info::NormalInfo, normal_media_url::NormalMediaUrl, player_info::PlayerInfo, + qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, skip_segments::SkipSegments, + subtitle::Subtitle, tags::Tags, user_info::UserInfo, user_video_info::UserVideoInfo, + watch_later_info::WatchLaterInfo, }, }; @@ -672,6 +674,53 @@ impl BiliClient { Ok(bangumi_follow_info) } + pub async fn get_history_info( + &self, + params: GetHistoryInfoParams, + ) -> anyhow::Result { + let device_type: i64 = params.device_type.into(); + let params = json!({ + "pn": params.pn, + "keyword": params.keyword, + "business": "archive", + "add_time_start": params.add_time_start, + "add_time_end": params.add_time_end, + "arc_max_duration": params.arc_max_duration, + "arc_min_duration": params.arc_min_duration, + "device_type": device_type, + }); + let request = self + .api_client + .read() + .get("https://api.bilibili.com/x/web-interface/history/search") + .query(¶ms) + .header("cookie", self.get_cookie()); + let http_resp = request.send().await?; + // 检查http响应状态码 + let status = http_resp.status(); + let body = http_resp.text().await?; + if status != StatusCode::OK { + return Err(anyhow!("预料之外的状态码({status}): {body}")); + } + // 尝试将body解析为BiliResp + let bili_resp: BiliResp = + serde_json::from_str(&body).context(format!("将body解析为BiliResp失败: {body}"))?; + // 检查BiliResp的code字段 + if bili_resp.code != 0 { + return Err(anyhow!("预料之外的code: {bili_resp:?}")); + } + // 检查BiliResp的data是否存在 + let Some(data) = bili_resp.data else { + return Err(anyhow!("BiliResp中不存在data字段: {bili_resp:?}")); + }; + // 尝试将data解析为HistoryInfo + let data_str = data.to_string(); + let history_info: HistoryInfo = serde_json::from_str(&data_str) + .context(format!("将data解析为HistoryInfo失败: {data_str}"))?; + + Ok(history_info) + } + pub async fn get_media_chunk( &self, media_url: &str, diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index f19f14e..313e161 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -18,8 +18,10 @@ use crate::{ get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams, get_fav_info_params::GetFavInfoParams, + get_history_info_params::GetHistoryInfoParams, get_normal_info_params::GetNormalInfoParams, get_user_video_info_params::GetUserVideoInfoParams, + history_info::HistoryInfo, normal_info::NormalInfo, qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, @@ -210,6 +212,20 @@ pub async fn get_bangumi_follow_info( Ok(bangumi_follow_info) } +#[tauri::command(async)] +#[specta::specta] +pub async fn get_history_info( + app: AppHandle, + params: GetHistoryInfoParams, +) -> CommandResult { + let bili_client = app.get_bili_client(); + let history_info = bili_client + .get_history_info(params) + .await + .map_err(|err| CommandError::from("获取历史记录失败", err))?; + Ok(history_info) +} + #[allow(clippy::needless_pass_by_value)] #[tauri::command(async)] #[specta::specta] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0862f71..0472855 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -46,6 +46,7 @@ pub fn run() { get_fav_info, get_watch_later_info, get_bangumi_follow_info, + get_history_info, create_download_tasks, pause_download_tasks, resume_download_tasks, diff --git a/src-tauri/src/types/get_history_info_params.rs b/src-tauri/src/types/get_history_info_params.rs new file mode 100644 index 0000000..8aa221f --- /dev/null +++ b/src-tauri/src/types/get_history_info_params.rs @@ -0,0 +1,38 @@ +use num_enum::{FromPrimitive, IntoPrimitive}; +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct GetHistoryInfoParams { + pub pn: i64, + pub keyword: String, + pub add_time_start: i64, + pub add_time_end: i64, + pub arc_max_duration: i64, + pub arc_min_duration: i64, + pub device_type: DeviceType, +} + +#[derive( + Default, + Debug, + Clone, + Copy, + Hash, + Eq, + PartialEq, + Serialize, + Deserialize, + Type, + IntoPrimitive, + FromPrimitive, +)] +#[repr(i64)] +pub enum DeviceType { + #[default] + All = 0, + PC = 1, + Mobile = 2, + Pad = 3, + TV = 4, +} diff --git a/src-tauri/src/types/history_info.rs b/src-tauri/src/types/history_info.rs new file mode 100644 index 0000000..c8046a7 --- /dev/null +++ b/src-tauri/src/types/history_info.rs @@ -0,0 +1,52 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct HistoryInfo { + pub has_more: bool, + pub page: PageInHistory, + pub list: Option>, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct PageInHistory { + pub pn: i64, + pub total: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct HistoryDetail { + pub title: String, + pub long_title: String, + pub cover: String, + pub uri: String, + pub history: History, + pub videos: i64, + pub author_name: String, + pub author_face: String, + pub author_mid: i64, + pub view_at: i64, + pub progress: i64, + pub badge: String, + pub show_title: String, + pub duration: i64, + pub total: i64, + pub new_desc: String, + pub is_finish: i64, + pub is_fav: i64, + pub kid: i64, + pub tag_name: String, + pub live_status: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct History { + pub oid: i64, + pub epid: i64, + pub bvid: String, + pub page: i64, + pub cid: i64, + pub part: String, + pub business: String, + pub dt: i64, +} diff --git a/src-tauri/src/types/mod.rs b/src-tauri/src/types/mod.rs index cf5e5de..b3cc9e7 100644 --- a/src-tauri/src/types/mod.rs +++ b/src-tauri/src/types/mod.rs @@ -12,8 +12,10 @@ pub mod get_bangumi_follow_info_params; pub mod get_bangumi_info_params; pub mod get_cheese_info_params; pub mod get_fav_info_params; +pub mod get_history_info_params; pub mod get_normal_info_params; pub mod get_user_video_info_params; +pub mod history_info; pub mod log_level; pub mod normal_info; pub mod normal_media_url; diff --git a/src/bindings.ts b/src/bindings.ts index 11d5586..4b2be01 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -96,6 +96,14 @@ async getBangumiFollowInfo(params: GetBangumiFollowInfoParams) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_history_info", { params }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async createDownloadTasks(params: CreateDownloadTaskParams) : Promise { await TAURI_INVOKE("create_download_tasks", { params }); }, @@ -259,6 +267,7 @@ export type CreateDownloadTaskParams = { Normal: CreateNormalDownloadTaskParams export type CreateNormalDownloadTaskParams = { info: NormalInfo; aid_cid_pairs: ([number, number | null])[] } export type DanmakuTask = { xml_selected: boolean; ass_selected: boolean; json_selected: boolean; completed: boolean } export type DescV2 = { raw_text: string; type: number; biz_id: number } +export type DeviceType = "All" | "PC" | "Mobile" | "Pad" | "TV" export type Dimension = { width: number; height: number; rotate: number } export type DimensionInBangumi = { height: number; rotate: number; width: number } export type DimensionInWatchLater = { width: number; height: number; rotate: number } @@ -290,8 +299,12 @@ type: number; pn: number; follow_status: number } export type GetBangumiInfoParams = { EpId: number } | { SeasonId: number } export type GetCheeseInfoParams = { EpId: number } | { SeasonId: number } export type GetFavInfoParams = { media_list_id: number; pn: number } +export type GetHistoryInfoParams = { pn: number; keyword: string; add_time_start: number; add_time_end: number; arc_max_duration: number; arc_min_duration: number; device_type: DeviceType } export type GetNormalInfoParams = { Bvid: string } | { Aid: number } export type GetUserVideoInfoParams = { pn: number; mid: number } +export type History = { oid: number; epid: number; bvid: string; page: number; cid: number; part: string; business: string; dt: number } +export type HistoryDetail = { title: string; long_title: string; cover: string; uri: string; history: History; videos: number; author_name: string; author_face: string; author_mid: number; view_at: number; progress: number; badge: string; show_title: string; duration: number; total: number; new_desc: string; is_finish: number; is_fav: number; kid: number; tag_name: string; live_status: number } +export type HistoryInfo = { has_more: boolean; page: PageInHistory; list: HistoryDetail[] | null } export type Honor = { aid: number; type: number; desc: string; weekly_recommend_num: number } export type HonorReply = { honor: Honor[] | null } export type IconFont = { name: string; text: string } @@ -319,6 +332,7 @@ export type OfficialVerify = { type: number; desc: string } export type Op = { end: number; start: number } export type OwnerInNormal = { mid: number; name: string; face: string } export type OwnerInWatchLater = { mid: number; name: string; face: string } +export type PageInHistory = { pn: number; total: number } export type PageInNormal = { cid: number; page: number; from: string; part: string; duration: number; vid: string; weblink: string; dimension: Dimension; ctime: number } export type PageInNormalEp = { cid: number; page: number; from: string; part: string; duration: number; vid: string; weblink: string; dimension: Dimension } export type PageInUserVideo = { pn: number; ps: number; count: number } From bc6ac4bcc37c07a9e068daf834460a8dbe1a0d4c Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Mon, 8 Sep 2025 06:06:30 +0800 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components.d.ts | 2 + src/App.vue | 8 +- src/AppContent.vue | 33 +- src/panes/HistoryPane/HistoryPane.vue | 43 +++ .../HistoryPane/components/HistoryCard.vue | 205 ++++++++++ .../HistoryPane/components/HistoryPanel.vue | 359 ++++++++++++++++++ 6 files changed, 640 insertions(+), 10 deletions(-) create mode 100644 src/panes/HistoryPane/HistoryPane.vue create mode 100644 src/panes/HistoryPane/components/HistoryCard.vue create mode 100644 src/panes/HistoryPane/components/HistoryPanel.vue diff --git a/components.d.ts b/components.d.ts index 266874d..ebbf1d5 100644 --- a/components.d.ts +++ b/components.d.ts @@ -16,6 +16,7 @@ declare module 'vue' { NCheckbox: typeof import('naive-ui')['NCheckbox'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] + NDatePicker: typeof import('naive-ui')['NDatePicker'] NDialog: typeof import('naive-ui')['NDialog'] NDialogProvider: typeof import('naive-ui')['NDialogProvider'] NDropdown: typeof import('naive-ui')['NDropdown'] @@ -31,6 +32,7 @@ declare module 'vue' { NModalProvider: typeof import('naive-ui')['NModalProvider'] NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] NPagination: typeof import('naive-ui')['NPagination'] + NPopover: typeof import('naive-ui')['NPopover'] NProgress: typeof import('naive-ui')['NProgress'] NQrCode: typeof import('naive-ui')['NQrCode'] NRadioButton: typeof import('naive-ui')['NRadioButton'] diff --git a/src/App.vue b/src/App.vue index 50aa4b8..48305c1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,6 @@