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 @@ + +