mirror of
https://github.com/bolucat/Archive.git
synced 2025-09-26 20:21:35 +08:00
Update On Sat Mar 22 19:35:00 CET 2025
This commit is contained in:
1
.github/update.log
vendored
1
.github/update.log
vendored
@@ -949,3 +949,4 @@ Update On Tue Mar 18 19:36:24 CET 2025
|
||||
Update On Wed Mar 19 19:36:39 CET 2025
|
||||
Update On Thu Mar 20 19:36:35 CET 2025
|
||||
Update On Fri Mar 21 19:35:22 CET 2025
|
||||
Update On Sat Mar 22 19:34:51 CET 2025
|
||||
|
4
clash-nyanpasu/backend/Cargo.lock
generated
4
clash-nyanpasu/backend/Cargo.lock
generated
@@ -7031,9 +7031,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.3"
|
||||
version = "1.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
||||
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
|
||||
|
||||
[[package]]
|
||||
name = "oneshot"
|
||||
|
@@ -11,7 +11,7 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "5.68.0",
|
||||
"@tanstack/react-query": "5.69.0",
|
||||
"@tauri-apps/api": "2.3.0",
|
||||
"ahooks": "3.8.4",
|
||||
"dayjs": "1.11.13",
|
||||
|
@@ -55,10 +55,10 @@
|
||||
"@emotion/react": "11.14.0",
|
||||
"@iconify/json": "2.2.319",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@tanstack/react-query": "5.68.0",
|
||||
"@tanstack/react-router": "1.114.25",
|
||||
"@tanstack/router-devtools": "1.114.25",
|
||||
"@tanstack/router-plugin": "1.114.25",
|
||||
"@tanstack/react-query": "5.69.0",
|
||||
"@tanstack/react-router": "1.114.27",
|
||||
"@tanstack/router-devtools": "1.114.27",
|
||||
"@tanstack/router-plugin": "1.114.27",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "2.2.0",
|
||||
"@tauri-apps/plugin-fs": "2.2.0",
|
||||
@@ -80,7 +80,7 @@
|
||||
"meta-json-schema": "1.19.3",
|
||||
"monaco-yaml": "5.3.1",
|
||||
"nanoid": "5.1.5",
|
||||
"sass-embedded": "1.85.1",
|
||||
"sass-embedded": "1.86.0",
|
||||
"shiki": "2.5.0",
|
||||
"unplugin-auto-import": "19.1.1",
|
||||
"unplugin-icons": "22.1.0",
|
||||
|
@@ -43,7 +43,7 @@
|
||||
"@types/d3-interpolate-path": "2.0.3",
|
||||
"clsx": "2.1.1",
|
||||
"d3-interpolate-path": "2.3.0",
|
||||
"sass-embedded": "1.85.1",
|
||||
"sass-embedded": "1.86.0",
|
||||
"tailwind-merge": "3.0.2",
|
||||
"typescript-plugin-css-modules": "5.1.0",
|
||||
"vite-plugin-dts": "4.5.3"
|
||||
|
@@ -60,19 +60,19 @@
|
||||
"@commitlint/cli": "19.8.0",
|
||||
"@commitlint/config-conventional": "19.8.0",
|
||||
"@eslint/compat": "1.2.7",
|
||||
"@eslint/eslintrc": "3.3.0",
|
||||
"@eslint/eslintrc": "3.3.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "4.4.1",
|
||||
"@tauri-apps/cli": "2.3.1",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/node": "22.13.11",
|
||||
"@typescript-eslint/eslint-plugin": "8.26.1",
|
||||
"@typescript-eslint/parser": "8.26.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.27.0",
|
||||
"@typescript-eslint/parser": "8.27.0",
|
||||
"autoprefixer": "10.4.21",
|
||||
"conventional-changelog-conventionalcommits": "8.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"dedent": "1.5.3",
|
||||
"eslint": "9.22.0",
|
||||
"eslint": "9.23.0",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-import-resolver-alias": "1.1.2",
|
||||
@@ -107,7 +107,7 @@
|
||||
"tailwindcss": "4.0.15",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "5.8.2",
|
||||
"typescript-eslint": "8.26.1"
|
||||
"typescript-eslint": "8.27.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.5",
|
||||
"engines": {
|
||||
|
757
clash-nyanpasu/pnpm-lock.yaml
generated
757
clash-nyanpasu/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@
|
||||
"zod": "3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/types": "13.8.0",
|
||||
"@octokit/types": "13.10.0",
|
||||
"@types/adm-zip": "0.5.7",
|
||||
"@types/yargs": "17.0.33",
|
||||
"adm-zip": "0.5.16",
|
||||
|
12
clash-verge-rev/.github/workflows/alpha.yml
vendored
12
clash-verge-rev/.github/workflows/alpha.yml
vendored
@@ -117,7 +117,8 @@ jobs:
|
||||
# Check if UPDATELOG.md exists
|
||||
if [ -f "UPDATELOG.md" ]; then
|
||||
# Extract the section starting with ## and containing -alpha until the next ## or end of file
|
||||
ALPHA_LOGS=$(awk '/^## .*-alpha/{flag=1; print; next} /^## /{flag=0} flag' UPDATELOG.md)
|
||||
# ALPHA_LOGS=$(awk '/^## .*-alpha/{flag=1; print; next} /^## /{flag=0} flag' UPDATELOG.md)
|
||||
ALPHA_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md)
|
||||
|
||||
if [ -n "$ALPHA_LOGS" ]; then
|
||||
echo "Found alpha update logs"
|
||||
@@ -246,6 +247,9 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
@@ -312,6 +316,9 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: "Setup for linux"
|
||||
run: |-
|
||||
sudo ls -lR /etc/apt/
|
||||
@@ -432,6 +439,9 @@ jobs:
|
||||
pnpm i
|
||||
pnpm check ${{ matrix.target }}
|
||||
|
||||
- name: Release Alpha Version
|
||||
run: pnpm release-alpha-version
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/109.0.1518.78/Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${{ matrix.arch }}.cab
|
||||
|
@@ -12,8 +12,15 @@
|
||||
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出。
|
||||
- 增加 首页文本过长自动截断
|
||||
- 修复 系统代理标识判断不准的问题
|
||||
- 修复 系统代理地址错误的问题
|
||||
2. **系统**
|
||||
- 增加 在 ClashVergeRev 对 Mihomo 进行操作时,总是尝试确保两者运行。
|
||||
- 修复 MacOS 无法使用快捷键粘贴/选择/复制订阅地址。
|
||||
- 修复 代理端口设置同步问题。
|
||||
- 修复 Linux 无法与 Mihomo 核心 和 ClashVergeRev 服务通信。
|
||||
- 增加 服务器模式下启动mihomo内核的时候查找并停止其他已经存在的内核进程,防止内核假死等问题带来的通信失败。
|
||||
3. **界面**
|
||||
- 修复 连接详情卡没有跟随主题色
|
||||
|
||||
---
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 314 KiB |
Binary file not shown.
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 274 KiB |
@@ -16,7 +16,8 @@
|
||||
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
|
||||
"portable": "node scripts/portable.mjs",
|
||||
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
|
||||
"fix-alpha-version": "node scripts/alpha_version.mjs",
|
||||
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
|
||||
"release-alpha-version": "node scripts/release-alpha_version.mjs",
|
||||
"prepare": "husky",
|
||||
"clean": "cd ./src-tauri && cargo clean && cd -"
|
||||
},
|
||||
|
96
clash-verge-rev/scripts/release-alpha_version.mjs
Normal file
96
clash-verge-rev/scripts/release-alpha_version.mjs
Normal file
@@ -0,0 +1,96 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* 更新 package.json 文件中的版本号
|
||||
*/
|
||||
async function updatePackageVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const packageJsonPath = path.join(_dirname, "package.json");
|
||||
try {
|
||||
const data = await fs.readFile(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(data);
|
||||
|
||||
let result = packageJson.version;
|
||||
if (!result.includes("alpha")) {
|
||||
result = `${result}-alpha`;
|
||||
}
|
||||
|
||||
console.log("[INFO]: Current package.json version is: ", result);
|
||||
packageJson.version = result;
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[INFO]: package.json version updated to: ${result}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating package.json version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Cargo.toml 文件中的版本号
|
||||
*/
|
||||
async function updateCargoVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const cargoTomlPath = path.join(_dirname, "src-tauri", "Cargo.toml");
|
||||
try {
|
||||
const data = await fs.readFile(cargoTomlPath, "utf8");
|
||||
const lines = data.split("\n");
|
||||
|
||||
const updatedLines = lines.map((line) => {
|
||||
if (line.startsWith("version =")) {
|
||||
const versionMatch = line.match(/version\s*=\s*"([^"]+)"/);
|
||||
if (versionMatch && !versionMatch[1].includes("alpha")) {
|
||||
const newVersion = `${versionMatch[1]}-alpha`;
|
||||
return line.replace(versionMatch[1], newVersion);
|
||||
}
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
|
||||
} catch (error) {
|
||||
console.error("Error updating Cargo.toml version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 tauri.conf.json 文件中的版本号
|
||||
*/
|
||||
async function updateTauriConfigVersion() {
|
||||
const _dirname = process.cwd();
|
||||
const tauriConfigPath = path.join(_dirname, "src-tauri", "tauri.conf.json");
|
||||
try {
|
||||
const data = await fs.readFile(tauriConfigPath, "utf8");
|
||||
const tauriConfig = JSON.parse(data);
|
||||
|
||||
let version = tauriConfig.version;
|
||||
if (!version.includes("alpha")) {
|
||||
version = `${version}-alpha`;
|
||||
}
|
||||
|
||||
console.log("[INFO]: Current tauri.conf.json version is: ", version);
|
||||
tauriConfig.version = version;
|
||||
await fs.writeFile(
|
||||
tauriConfigPath,
|
||||
JSON.stringify(tauriConfig, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
console.log(`[INFO]: tauri.conf.json version updated to: ${version}`);
|
||||
} catch (error) {
|
||||
console.error("Error updating tauri.conf.json version:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数,依次更新所有文件的版本号
|
||||
*/
|
||||
async function main() {
|
||||
await updatePackageVersion();
|
||||
await updateCargoVersion();
|
||||
await updateTauriConfigVersion();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
11
clash-verge-rev/src-tauri/Cargo.lock
generated
11
clash-verge-rev/src-tauri/Cargo.lock
generated
@@ -1146,6 +1146,7 @@ dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
"env_logger",
|
||||
"fs2",
|
||||
"futures",
|
||||
"getrandom 0.3.2",
|
||||
"image",
|
||||
@@ -2339,6 +2340,16 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
|
@@ -34,6 +34,7 @@ port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.6"
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3.1"
|
||||
fs2 = "0.4.3"
|
||||
window-shadows = { version = "0.2.2" }
|
||||
tokio = { version = "1.43", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@@ -1,8 +1,9 @@
|
||||
use super::CmdResult;
|
||||
use crate::module::mihomo::MihomoManager;
|
||||
use crate::{core::CoreManager, module::mihomo::MihomoManager};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
let mannager = MihomoManager::global();
|
||||
let proxies = mannager
|
||||
.refresh_proxies()
|
||||
@@ -14,6 +15,7 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
let mannager = MihomoManager::global();
|
||||
let providers = mannager
|
||||
.refresh_providers_proxies()
|
||||
|
@@ -8,11 +8,14 @@ use crate::{
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use fs2::FileExt;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::{sync::Mutex, time::sleep};
|
||||
|
||||
use super::service::is_service_running;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CoreManager {
|
||||
running: Arc<Mutex<bool>>,
|
||||
@@ -50,10 +53,13 @@ impl CoreManager {
|
||||
let mut running = self.running.lock().await;
|
||||
|
||||
if !*running {
|
||||
println!("[停止内核] 内核未运行");
|
||||
log::debug!("core is not running");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("[停止内核] 开始停止内核");
|
||||
|
||||
// 关闭tun模式
|
||||
// Create a JSON object to disable TUN mode
|
||||
let disable = serde_json::json!({
|
||||
@@ -61,17 +67,22 @@ impl CoreManager {
|
||||
"enable": false
|
||||
}
|
||||
});
|
||||
println!("[停止内核] 禁用TUN模式");
|
||||
log::debug!(target: "app", "disable tun mode");
|
||||
log_err!(MihomoManager::global().patch_configs(disable).await);
|
||||
|
||||
// 服务模式
|
||||
if service::check_service().await.is_ok() {
|
||||
println!("[停止内核] 尝试通过服务停止内核");
|
||||
log::info!(target: "app", "stop the core by service");
|
||||
match service::stop_core_by_service().await {
|
||||
Ok(_) => {
|
||||
println!("[停止内核] 服务模式下内核停止成功");
|
||||
log::info!(target: "app", "core stopped successfully by service");
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[停止内核] 服务模式下停止内核失败: {}", err);
|
||||
println!("[停止内核] 尝试停止可能的sidecar进程");
|
||||
log::warn!(target: "app", "failed to stop core by service: {}", err);
|
||||
// 服务停止失败,尝试停止可能的sidecar进程
|
||||
self.stop_sidecar_process();
|
||||
@@ -79,22 +90,44 @@ impl CoreManager {
|
||||
}
|
||||
} else {
|
||||
// 如果没有使用服务,尝试停止sidecar进程
|
||||
println!("[停止内核] 服务不可用,尝试停止sidecar进程");
|
||||
self.stop_sidecar_process();
|
||||
}
|
||||
|
||||
// 释放文件锁
|
||||
println!("[停止内核] 尝试释放文件锁");
|
||||
if let Some(_) = handle::Handle::global().release_core_lock() {
|
||||
println!("[停止内核] 文件锁释放成功");
|
||||
log::info!(target: "app", "released core lock file");
|
||||
} else {
|
||||
println!("[停止内核] 没有文件锁需要释放");
|
||||
}
|
||||
|
||||
*running = false;
|
||||
println!("[停止内核] 内核停止完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止通过sidecar启动的进程
|
||||
fn stop_sidecar_process(&self) {
|
||||
if let Some(process) = handle::Handle::global().take_core_process() {
|
||||
println!("[停止sidecar] 发现sidecar进程,准备停止");
|
||||
log::info!(target: "app", "stopping core process in sidecar mode");
|
||||
|
||||
// 尝试获取进程ID
|
||||
let pid = process.pid();
|
||||
println!("[停止sidecar] 进程PID: {}", pid);
|
||||
|
||||
// 尝试终止进程
|
||||
if let Err(e) = process.kill() {
|
||||
println!("[停止sidecar] 终止sidecar进程失败: {}", e);
|
||||
log::warn!(target: "app", "failed to kill core process: {}", e);
|
||||
} else {
|
||||
println!("[停止sidecar] sidecar进程已成功终止");
|
||||
log::info!(target: "app", "core process stopped successfully");
|
||||
}
|
||||
} else {
|
||||
println!("[停止sidecar] 没有找到sidecar进程");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +176,99 @@ impl CoreManager {
|
||||
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
|
||||
|
||||
log::info!(target: "app", "starting core {} in sidecar mode", clash_core);
|
||||
println!("[sidecar启动] 开始以sidecar模式启动内核: {}", clash_core);
|
||||
|
||||
// 检查系统中是否存在同名进程
|
||||
if let Ok(pids) = self.check_existing_processes(&clash_core).await {
|
||||
if !pids.is_empty() {
|
||||
println!("[sidecar启动] 警告:系统中已存在同名进程");
|
||||
// 尝试检查端口占用
|
||||
if let Ok(config_content) = std::fs::read_to_string(config_path) {
|
||||
if let Ok(config) = serde_yaml::from_str::<serde_yaml::Value>(&config_content) {
|
||||
// 获取配置中定义的端口
|
||||
let mixed_port = config
|
||||
.get("mixed-port")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(7890);
|
||||
let http_port = config.get("port").and_then(|v| v.as_u64()).unwrap_or(7890);
|
||||
|
||||
println!(
|
||||
"[sidecar启动] 检查端口占用: HTTP端口={}, 混合端口={}",
|
||||
http_port, mixed_port
|
||||
);
|
||||
|
||||
// 检查端口是否被占用
|
||||
if self.is_port_in_use(mixed_port as u16).await
|
||||
|| self.is_port_in_use(http_port as u16).await
|
||||
{
|
||||
println!("[sidecar启动] 端口已被占用,尝试终止已存在的进程");
|
||||
|
||||
// 尝试终止已存在的进程
|
||||
for pid in pids {
|
||||
println!("[sidecar启动] 尝试终止进程 PID: {}", pid);
|
||||
self.terminate_process(pid).await;
|
||||
}
|
||||
|
||||
// 等待短暂时间让资源释放
|
||||
println!("[sidecar启动] 等待500ms让资源释放");
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("[sidecar启动] 无法检查系统进程,继续尝试启动");
|
||||
}
|
||||
|
||||
// 创建锁文件路径
|
||||
let lock_file = dirs::app_home_dir()?.join(format!("{}.lock", clash_core));
|
||||
println!("[sidecar启动] 锁文件路径: {:?}", lock_file);
|
||||
|
||||
// 尝试获取文件锁
|
||||
println!("[sidecar启动] 尝试获取文件锁");
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&lock_file)?;
|
||||
|
||||
match file.try_lock_exclusive() {
|
||||
Ok(_) => {
|
||||
// 成功获取锁,说明没有其他实例运行
|
||||
println!("[sidecar启动] 成功获取文件锁,没有检测到其他运行的实例");
|
||||
log::info!(target: "app", "acquired lock for core process");
|
||||
|
||||
// 保存锁对象到全局,防止被Drop
|
||||
handle::Handle::global().set_core_lock(file);
|
||||
}
|
||||
Err(err) => {
|
||||
// 无法获取锁,说明已有实例运行
|
||||
println!("[sidecar启动] 无法获取文件锁,检测到其他实例可能正在运行");
|
||||
println!("[sidecar启动] 错误信息: {:?}", err);
|
||||
log::warn!(target: "app", "another core process appears to be running");
|
||||
|
||||
// 尝试强制获取锁(可能会导致其他进程崩溃)
|
||||
println!("[sidecar启动] 尝试强制删除并重新创建锁文件");
|
||||
std::fs::remove_file(&lock_file)?;
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&lock_file)?;
|
||||
|
||||
println!("[sidecar启动] 尝试强制获取锁");
|
||||
match file.lock_exclusive() {
|
||||
Ok(_) => println!("[sidecar启动] 成功强制获取锁"),
|
||||
Err(e) => println!("[sidecar启动] 强制获取锁失败: {:?}", e),
|
||||
}
|
||||
file.lock_exclusive()?;
|
||||
|
||||
// 保存新锁
|
||||
handle::Handle::global().set_core_lock(file);
|
||||
|
||||
// 等待可能的其他进程退出
|
||||
println!("[sidecar启动] 等待500ms,让可能的其他进程退出");
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let app_handle = handle::Handle::global()
|
||||
.app_handle()
|
||||
@@ -153,6 +279,7 @@ impl CoreManager {
|
||||
let config_path_str = dirs::path_to_str(config_path)?;
|
||||
|
||||
// 启动核心进程并转入后台运行
|
||||
println!("[sidecar启动] 开始启动核心进程");
|
||||
let (_, child) = app_handle
|
||||
.shell()
|
||||
.sidecar(clash_core)?
|
||||
@@ -160,11 +287,13 @@ impl CoreManager {
|
||||
.spawn()?;
|
||||
|
||||
// 保存进程ID以便后续管理
|
||||
println!("[sidecar启动] 核心进程启动成功,PID: {:?}", child.pid());
|
||||
handle::Handle::global().set_core_process(child);
|
||||
|
||||
// 等待短暂时间确保启动成功
|
||||
sleep(Duration::from_millis(300)).await;
|
||||
|
||||
println!("[sidecar启动] 内核启动完成");
|
||||
log::info!(target: "app", "core started in sidecar mode");
|
||||
Ok(())
|
||||
}
|
||||
@@ -538,6 +667,8 @@ impl CoreManager {
|
||||
// 5. 应用新配置
|
||||
println!("[core配置更新] 应用新配置");
|
||||
for i in 0..3 {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
|
||||
match MihomoManager::global().put_configs_force(run_path).await {
|
||||
Ok(_) => {
|
||||
println!("[core配置更新] 配置应用成功");
|
||||
@@ -618,7 +749,14 @@ impl CoreManager {
|
||||
_ => {
|
||||
// 服务存在但可能没有运行,检查是否有sidecar进程
|
||||
if handle::Handle::global().has_core_process() {
|
||||
RunningMode::Sidecar
|
||||
// 检查是否持有文件锁,确保是由我们启动的进程
|
||||
if handle::Handle::global().has_core_lock() {
|
||||
RunningMode::Sidecar
|
||||
} else {
|
||||
// 有进程但没有文件锁,可能是外部启动的进程
|
||||
log::warn!(target: "app", "core process exists but no lock file");
|
||||
RunningMode::Sidecar // 仍返回Sidecar模式,但记录了警告
|
||||
}
|
||||
} else {
|
||||
RunningMode::NotRunning
|
||||
}
|
||||
@@ -628,11 +766,226 @@ impl CoreManager {
|
||||
Err(_) => {
|
||||
// 服务不可用,检查是否有sidecar进程
|
||||
if handle::Handle::global().has_core_process() {
|
||||
RunningMode::Sidecar
|
||||
// 检查是否持有文件锁,确保是由我们启动的进程
|
||||
if handle::Handle::global().has_core_lock() {
|
||||
RunningMode::Sidecar
|
||||
} else {
|
||||
// 有进程但没有文件锁,可能是外部启动的进程
|
||||
log::warn!(target: "app", "core process exists but no lock file");
|
||||
RunningMode::Sidecar // 仍返回Sidecar模式,但记录了警告
|
||||
}
|
||||
} else {
|
||||
RunningMode::NotRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查系统中是否存在同名进程
|
||||
async fn check_existing_processes(&self, process_name: &str) -> Result<Vec<u32>> {
|
||||
println!("[进程检查] 检查系统中是否存在进程: {}", process_name);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
|
||||
println!("[进程检查] Windows系统,使用tasklist命令");
|
||||
let output = Command::new("tasklist")
|
||||
.args(["/FO", "CSV", "/NH"])
|
||||
.output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let pids: Vec<u32> = output
|
||||
.lines()
|
||||
.filter(|line| line.contains(process_name))
|
||||
.filter_map(|line| {
|
||||
println!("[进程检查] 发现匹配行: {}", line);
|
||||
let parts: Vec<&str> = line.split(',').collect();
|
||||
if parts.len() >= 2 {
|
||||
let pid_str = parts[1].trim_matches('"');
|
||||
pid_str.parse::<u32>().ok().map(|pid| {
|
||||
println!("[进程检查] 发现进程 PID: {}", pid);
|
||||
pid
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("[进程检查] 共发现 {} 个相关进程", pids.len());
|
||||
Ok(pids)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::process::Command;
|
||||
|
||||
println!("[进程检查] Linux系统,使用pgrep命令");
|
||||
let output = Command::new("pgrep")
|
||||
.arg("-f")
|
||||
.arg(process_name)
|
||||
.output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let pids: Vec<u32> = output
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
line.trim().parse::<u32>().ok().map(|pid| {
|
||||
println!("[进程检查] 发现进程 PID: {}", pid);
|
||||
pid
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("[进程检查] 共发现 {} 个相关进程", pids.len());
|
||||
Ok(pids)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Command;
|
||||
|
||||
println!("[进程检查] macOS系统,使用ps命令");
|
||||
let output = Command::new("ps")
|
||||
.args(["-ax", "-o", "pid,command"])
|
||||
.output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let pids: Vec<u32> = output
|
||||
.lines()
|
||||
.filter(|line| line.contains(process_name))
|
||||
.filter_map(|line| {
|
||||
println!("[进程检查] 发现匹配行: {}", line);
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
parts[0].parse::<u32>().ok().map(|pid| {
|
||||
println!("[进程检查] 发现进程 PID: {}", pid);
|
||||
pid
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("[进程检查] 共发现 {} 个相关进程", pids.len());
|
||||
Ok(pids)
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查端口是否被占用
|
||||
async fn is_port_in_use(&self, port: u16) -> bool {
|
||||
println!("[端口检查] 检查端口 {} 是否被占用", port);
|
||||
|
||||
use tokio::net::TcpSocket;
|
||||
|
||||
match TcpSocket::new_v4() {
|
||||
Ok(socket) => {
|
||||
let addr = format!("127.0.0.1:{}", port).parse().unwrap();
|
||||
match socket.bind(addr) {
|
||||
Ok(_) => {
|
||||
// 如果能绑定成功,说明端口未被占用
|
||||
println!("[端口检查] 端口 {} 未被占用", port);
|
||||
false
|
||||
}
|
||||
Err(_) => {
|
||||
// 绑定失败,端口已被占用
|
||||
println!("[端口检查] 端口 {} 已被占用", port);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// 创建socket失败,保守返回端口被占用
|
||||
println!("[端口检查] 创建Socket失败: {:?}, 假设端口已被占用", err);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 终止进程
|
||||
async fn terminate_process(&self, pid: u32) {
|
||||
println!("[进程终止] 尝试终止进程 PID: {}", pid);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let output = Command::new("taskkill")
|
||||
.args(["/F", "/PID", &pid.to_string()])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
println!("[进程终止] 成功终止进程 PID: {}", pid);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("[进程终止] 终止进程失败: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[进程终止] 执行终止命令失败: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let output = Command::new("kill").args(["-9", &pid.to_string()]).output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
println!("[进程终止] 成功终止进程 PID: {}", pid);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("[进程终止] 终止进程失败: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[进程终止] 执行终止命令失败: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let output = Command::new("kill").args(["-9", &pid.to_string()]).output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
println!("[进程终止] 成功终止进程 PID: {}", pid);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
println!("[进程终止] 终止进程失败: {}", stderr);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[进程终止] 执行终止命令失败: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 确保 Mihomo 和 Verge service 都在运行
|
||||
pub async fn ensure_running_core(&self) {
|
||||
if MihomoManager::global().is_mihomo_running().await.is_err() {
|
||||
log_err!(self.restart_core().await);
|
||||
}
|
||||
match is_service_running().await {
|
||||
Ok(false) => log_err!(self.restart_core().await),
|
||||
Ok(true) => {
|
||||
if MihomoManager::global().is_mihomo_running().await.is_err() {
|
||||
log_err!(self.restart_core().await);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,12 +4,14 @@ use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
|
||||
use tauri_plugin_shell::process::CommandChild;
|
||||
use std::fs::File;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Handle {
|
||||
pub app_handle: Arc<RwLock<Option<AppHandle>>>,
|
||||
pub is_exiting: Arc<RwLock<bool>>,
|
||||
pub core_process: Arc<RwLock<Option<CommandChild>>>,
|
||||
pub core_lock: Arc<RwLock<Option<File>>>,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
@@ -20,6 +22,7 @@ impl Handle {
|
||||
app_handle: Arc::new(RwLock::new(None)),
|
||||
is_exiting: Arc::new(RwLock::new(false)),
|
||||
core_process: Arc::new(RwLock::new(None)),
|
||||
core_lock: Arc::new(RwLock::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,4 +92,21 @@ impl Handle {
|
||||
pub fn is_exiting(&self) -> bool {
|
||||
*self.is_exiting.read()
|
||||
}
|
||||
|
||||
/// 设置核心文件锁
|
||||
pub fn set_core_lock(&self, file: File) {
|
||||
let mut core_lock = self.core_lock.write();
|
||||
*core_lock = Some(file);
|
||||
}
|
||||
|
||||
/// 释放核心文件锁
|
||||
pub fn release_core_lock(&self) -> Option<File> {
|
||||
let mut core_lock = self.core_lock.write();
|
||||
core_lock.take()
|
||||
}
|
||||
|
||||
/// 检查是否持有核心文件锁
|
||||
pub fn has_core_lock(&self) -> bool {
|
||||
self.core_lock.read().is_some()
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ use tokio::time::Duration;
|
||||
// Windows only
|
||||
|
||||
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
||||
const REQUIRED_SERVICE_VERSION: &str = "1.0.2"; // 定义所需的服务版本号
|
||||
const REQUIRED_SERVICE_VERSION: &str = "1.0.3"; // 定义所需的服务版本号
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ResponseBody {
|
||||
@@ -250,6 +250,8 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
let config_dir = dirs::path_to_str(&config_dir)?;
|
||||
#[cfg(target_os = "linux")]
|
||||
let config_dir = &(config_dir.replace("/verge-mihomo", "") + "/resources");
|
||||
|
||||
let log_path = dirs::service_log_file()?;
|
||||
let log_path = dirs::path_to_str(&log_path)?;
|
||||
|
@@ -21,6 +21,10 @@ use parking_lot::RwLock;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use speed_rate::{SpeedRate, Traffic};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::hash::{Hash, Hasher};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::sync::Arc;
|
||||
use tauri::{
|
||||
menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
|
||||
@@ -36,6 +40,9 @@ pub struct Tray {
|
||||
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
|
||||
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
|
||||
is_subscribed: Arc<RwLock<bool>>,
|
||||
pub icon_hash: Arc<Mutex<Option<u64>>>,
|
||||
pub icon_cache: Arc<Mutex<Option<Vec<u8>>>>,
|
||||
pub rate_cache: Arc<Mutex<Option<Rate>>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -50,6 +57,9 @@ impl Tray {
|
||||
speed_rate: Arc::new(Mutex::new(None)),
|
||||
shutdown_tx: Arc::new(RwLock::new(None)),
|
||||
is_subscribed: Arc::new(RwLock::new(false)),
|
||||
icon_hash: Arc::new(Mutex::new(None)),
|
||||
icon_cache: Arc::new(Mutex::new(None)),
|
||||
rate_cache: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
@@ -228,34 +238,59 @@ impl Tray {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true);
|
||||
let enable_tray_speed = verge.enable_tray_speed.unwrap_or(true);
|
||||
let is_colorful = tray_icon == "colorful";
|
||||
|
||||
// 处理图标和速率
|
||||
let final_icon_bytes = if enable_tray_speed {
|
||||
let rate = rate.or_else(|| {
|
||||
self.speed_rate
|
||||
.lock()
|
||||
.as_ref()
|
||||
.and_then(|speed_rate| speed_rate.get_curent_rate())
|
||||
});
|
||||
|
||||
// 使用新的方法渲染图标和速率
|
||||
SpeedRate::add_speed_text(icon_bytes, rate)?
|
||||
} else {
|
||||
icon_bytes
|
||||
let icon_hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
icon_bytes.clone().hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
// 设置系统托盘图标
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&final_icon_bytes)?));
|
||||
// 只对单色图标使用 template 模式
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
let mut icon_hash_guard = self.icon_hash.lock();
|
||||
let mut icon_bytes_guard = self.icon_cache.lock();
|
||||
if *icon_hash_guard != Some(icon_hash) {
|
||||
*icon_hash_guard = Some(icon_hash);
|
||||
*icon_bytes_guard = Some(icon_bytes.clone());
|
||||
}
|
||||
|
||||
if !enable_tray_speed {
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(
|
||||
&(*icon_bytes_guard).clone().unwrap(),
|
||||
)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rate = if let Some(rate) = rate {
|
||||
Some(rate)
|
||||
} else {
|
||||
let guard = self.speed_rate.lock();
|
||||
if let Some(rate) = guard.as_ref().unwrap().get_curent_rate() {
|
||||
Some(rate)
|
||||
} else {
|
||||
Some(Rate::default())
|
||||
}
|
||||
};
|
||||
|
||||
let mut rate_guard = self.rate_cache.lock();
|
||||
if *rate_guard != rate {
|
||||
*rate_guard = rate;
|
||||
|
||||
let bytes = icon_bytes_guard.as_ref().unwrap();
|
||||
let rate = rate_guard.as_ref();
|
||||
let rate_bytes = SpeedRate::add_speed_text(bytes, rate).unwrap();
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?));
|
||||
let _ = tray.set_icon_as_template(!is_colorful);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
|
||||
Ok(())
|
||||
{
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新托盘提示
|
||||
|
@@ -15,7 +15,6 @@ use tungstenite::client::IntoClientRequest;
|
||||
pub struct SpeedRate {
|
||||
rate: Arc<Mutex<(Rate, Rate)>>,
|
||||
last_update: Arc<Mutex<std::time::Instant>>,
|
||||
// 移除 base_image,不再缓存原始图像
|
||||
}
|
||||
|
||||
impl SpeedRate {
|
||||
@@ -77,20 +76,24 @@ impl SpeedRate {
|
||||
}
|
||||
|
||||
// 分离图标加载和速率渲染
|
||||
pub fn add_speed_text(icon_bytes: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
|
||||
let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
|
||||
pub fn add_speed_text<'a>(icon_bytes: &'a Vec<u8>, rate: Option<&'a Rate>) -> Result<Vec<u8>> {
|
||||
let rate = rate.unwrap_or(&Rate { up: 0, down: 0 });
|
||||
|
||||
// 加载原始图标
|
||||
let icon_image = image::load_from_memory(&icon_bytes)?;
|
||||
let icon_image = image::load_from_memory(icon_bytes)?;
|
||||
let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
|
||||
|
||||
// 判断是否为彩色图标
|
||||
let is_colorful =
|
||||
!crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
|
||||
!crate::utils::help::is_monochrome_image_from_bytes(icon_bytes).unwrap_or(false);
|
||||
|
||||
// 增加文本宽度和间距
|
||||
let text_width = 580; // 文本区域宽度
|
||||
let total_width = icon_width + text_width;
|
||||
let total_width = if icon_width < 580 {
|
||||
let text_width = 580; // 文本区域宽度
|
||||
icon_width + text_width
|
||||
} else {
|
||||
icon_width
|
||||
};
|
||||
|
||||
// 创建新的透明画布
|
||||
let mut combined_image = RgbaImage::new(total_width, icon_height);
|
||||
|
@@ -47,6 +47,7 @@ pub fn change_clash_mode(mode: String) {
|
||||
});
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
|
||||
match MihomoManager::global().patch_configs(json_value).await {
|
||||
Ok(_) => {
|
||||
@@ -66,6 +67,7 @@ pub fn change_clash_mode(mode: String) {
|
||||
|
||||
/// Test connection delay to a URL
|
||||
pub async fn test_delay(url: String) -> anyhow::Result<u32> {
|
||||
CoreManager::global().ensure_running_core().await;
|
||||
use tokio::time::{Duration, Instant};
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
|
@@ -98,6 +98,12 @@ impl MihomoManager {
|
||||
}
|
||||
|
||||
impl MihomoManager {
|
||||
pub async fn is_mihomo_running(&self) -> Result<(), String> {
|
||||
let url = format!("{}/version", self.mihomo_server);
|
||||
let _response = self.send_request(Method::GET, url, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), String> {
|
||||
let url = format!("{}/configs?force=true", self.mihomo_server);
|
||||
let payload = serde_json::json!({
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, Button, Snackbar } from "@mui/material";
|
||||
import { Box, Button, Snackbar, useTheme } from "@mui/material";
|
||||
import { deleteConnection } from "@/services/api";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { t } from "i18next";
|
||||
@@ -14,6 +14,7 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
(props, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
||||
const theme = useTheme();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: (detail: IConnectionsItem) => {
|
||||
@@ -35,6 +36,8 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
maxWidth: "520px",
|
||||
maxHeight: "480px",
|
||||
overflowY: "auto",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
message={
|
||||
@@ -54,6 +57,7 @@ interface InnerProps {
|
||||
|
||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const { metadata, rulePayload } = data;
|
||||
const theme = useTheme();
|
||||
const chains = [...data.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||
const host = metadata.host
|
||||
@@ -99,11 +103,11 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||
|
||||
return (
|
||||
<Box sx={{ userSelect: "text" }}>
|
||||
<Box sx={{ userSelect: "text", color: theme.palette.text.secondary }}>
|
||||
{information.map((each) => (
|
||||
<div key={each.label}>
|
||||
<b>{each.label}</b>
|
||||
<span style={{ wordBreak: "break-all" }}>: {each.value}</span>
|
||||
<span style={{ wordBreak: "break-all", color: theme.palette.text.primary }}>: {each.value}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
@@ -6,8 +6,8 @@ import { useClash } from "@/hooks/use-clash";
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
import useSWR from "swr";
|
||||
import { getRules } from "@/services/api";
|
||||
import { getAppUptime } from "@/services/cmds";
|
||||
import { useMemo } from "react";
|
||||
import { getAppUptime, getSystemProxy } from "@/services/cmds";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
|
||||
// 将毫秒转换为时:分:秒格式的函数
|
||||
const formatUptime = (uptimeMs: number) => {
|
||||
@@ -21,6 +21,8 @@ export const ClashInfoCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { clashInfo } = useClashInfo();
|
||||
const { version: clashVersion } = useClash();
|
||||
const [sysproxy, setSysproxy] = useState<{ server: string; enable: boolean; bypass: string } | null>(null);
|
||||
const [rules, setRules] = useState<any[]>([]);
|
||||
|
||||
// 使用SWR获取应用运行时间,降低更新频率
|
||||
const { data: uptimeMs = 0 } = useSWR(
|
||||
@@ -33,15 +35,18 @@ export const ClashInfoCard = () => {
|
||||
},
|
||||
);
|
||||
|
||||
// 在组件加载时获取系统代理信息和规则数据
|
||||
useEffect(() => {
|
||||
// 获取系统代理信息
|
||||
getSystemProxy().then(setSysproxy);
|
||||
|
||||
// 获取规则数据
|
||||
getRules().then(setRules).catch(() => setRules([]));
|
||||
}, []);
|
||||
|
||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||
const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]);
|
||||
|
||||
// 获取规则数据,只在组件加载时获取一次
|
||||
const { data: rules = [] } = useSWR("getRules", getRules, {
|
||||
revalidateOnFocus: false,
|
||||
errorRetryCount: 2,
|
||||
});
|
||||
|
||||
// 使用备忘录组件内容,减少重新渲染
|
||||
const cardContent = useMemo(() => {
|
||||
if (!clashInfo) return null;
|
||||
@@ -62,7 +67,7 @@ export const ClashInfoCard = () => {
|
||||
{t("System Proxy Address")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashInfo.server || "-"}
|
||||
{sysproxy?.server || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
@@ -94,7 +99,7 @@ export const ClashInfoCard = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}, [clashInfo, clashVersion, t, uptime, rules.length]);
|
||||
}, [clashInfo, clashVersion, t, uptime, rules.length, sysproxy]);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
|
@@ -280,16 +280,11 @@ export const CurrentProxyCard = () => {
|
||||
}
|
||||
}, [currentProxy, fetchProxyData, state.displayProxy]);
|
||||
|
||||
// 平滑的定期刷新,使用固定间隔
|
||||
// 监听模式变化,mode变化时刷新
|
||||
useEffect(() => {
|
||||
fetchProxyData();
|
||||
fetchProxyData(true);
|
||||
}, [mode, fetchProxyData]);
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
fetchProxyData();
|
||||
}, 3000); // 使用固定的3秒间隔,平衡响应速度和性能
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [fetchProxyData]);
|
||||
|
||||
// 计算要显示的代理选项 - 使用 useMemo 优化
|
||||
const proxyOptions = useMemo(() => {
|
||||
|
34
echo/.github/workflows/cd.yaml
vendored
34
echo/.github/workflows/cd.yaml
vendored
@@ -17,7 +17,7 @@ env:
|
||||
|
||||
jobs:
|
||||
prepare-matrix:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT
|
||||
|
||||
build-bin:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.21"
|
||||
go-version: "1.22"
|
||||
id: go
|
||||
|
||||
# Updated cache step
|
||||
@@ -52,12 +52,15 @@ jobs:
|
||||
|
||||
- name: build
|
||||
run: |
|
||||
rm -rf /tmp/ehco
|
||||
mkdir -p /tmp/ehco
|
||||
make build
|
||||
mv ./dist/ehco /tmp/ehco/ehco-amd64
|
||||
make build-arm
|
||||
mv ./dist/ehco /tmp/ehco/ehco-arm64
|
||||
cp ./build/Dockerfile /tmp/ehco/Dockerfile
|
||||
# debug
|
||||
ls -la /tmp/ehco
|
||||
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -68,6 +71,7 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
build-image:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare-matrix
|
||||
@@ -81,9 +85,14 @@ jobs:
|
||||
with:
|
||||
name: binaries
|
||||
path: /tmp/ehco
|
||||
|
||||
- name: Rename the binary
|
||||
run: |
|
||||
# debug
|
||||
ls -la /tmp/ehco
|
||||
cp /tmp/ehco/ehco-${{ matrix.platform }} /tmp/ehco/ehco
|
||||
# debug
|
||||
ls -la /tmp/ehco
|
||||
|
||||
- name: Set up qemu
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -123,6 +132,7 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event_name == 'push' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare-matrix
|
||||
@@ -164,10 +174,22 @@ jobs:
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
echo "Creating manifest list..."
|
||||
AMD64_DIGEST=$(ls amd64/)
|
||||
ARM64_DIGEST=$(ls arm64/)
|
||||
# debug
|
||||
ls -la /tmp/digests
|
||||
ls -la /tmp/digests/amd64 || echo "amd64 digest not found"
|
||||
ls -la /tmp/digests/arm64 || echo "arm64 digest not found"
|
||||
|
||||
AMD64_DIGEST=$(ls amd64/ 2>/dev/null || echo "")
|
||||
ARM64_DIGEST=$(ls arm64/ 2>/dev/null || echo "")
|
||||
|
||||
echo "AMD64_DIGEST: $AMD64_DIGEST"
|
||||
echo "ARM64_DIGEST: $ARM64_DIGEST"
|
||||
|
||||
if [ -z "$AMD64_DIGEST" ] || [ -z "$ARM64_DIGEST" ]; then
|
||||
echo "Error: One or more digests are missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker buildx imagetools create \
|
||||
--tag ${{ env.REGISTRY_IMAGE }}:latest \
|
||||
${{ env.REGISTRY_IMAGE }}@sha256:$AMD64_DIGEST \
|
||||
@@ -175,4 +197,4 @@ jobs:
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version || 'latest' }}
|
||||
|
@@ -7,11 +7,11 @@ type Config struct {
|
||||
}
|
||||
|
||||
func (c *Config) NeedSync() bool {
|
||||
return c.SyncURL != ""
|
||||
return c.SyncURL != "" && c.SyncInterval > 0
|
||||
}
|
||||
|
||||
func (c *Config) NeedMetrics() bool {
|
||||
return c.MetricsURL != ""
|
||||
return c.MetricsURL != "" && c.SyncInterval > 0
|
||||
}
|
||||
|
||||
func (c *Config) Adjust() {
|
||||
|
@@ -128,6 +128,10 @@ func (c *Config) NeedStartRelayServer() bool {
|
||||
return len(c.RelayConfigs) > 0
|
||||
}
|
||||
|
||||
func (c *Config) NeedStartCmgr() bool {
|
||||
return c.RelaySyncURL != "" && c.RelaySyncInterval > 0
|
||||
}
|
||||
|
||||
func (c *Config) GetMetricURL() string {
|
||||
if !c.NeedStartWebServer() {
|
||||
return ""
|
||||
|
@@ -80,8 +80,10 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
go s.WatchAndReload(ctx)
|
||||
}
|
||||
|
||||
// start Cmgr
|
||||
go s.Cmgr.Start(ctx, s.errCH)
|
||||
// start Cmgr when need sync from server
|
||||
if s.cfg.NeedStartCmgr() {
|
||||
go s.Cmgr.Start(ctx, s.errCH)
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-s.errCH:
|
||||
|
@@ -172,7 +172,7 @@ func (xs *XrayServer) Start(ctx context.Context) error {
|
||||
if err := xs.Reload(); err != nil {
|
||||
xs.l.Error("Reload Xray Server meet error", zap.Error(err))
|
||||
}
|
||||
xs.l.Warn("Reload Xray Server success exit watcher ...")
|
||||
xs.l.Info("Reload Xray Server Once success")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@@ -80,11 +80,6 @@ define Host/SetToolchainInfo
|
||||
endef
|
||||
|
||||
define Host/Configure
|
||||
[ -f $(HOST_BUILD_DIR)/.autoconf ] || { \
|
||||
cd $(HOST_BUILD_DIR)/; \
|
||||
autoconf --force && \
|
||||
touch $(HOST_BUILD_DIR)/.autoconf; \
|
||||
}
|
||||
mkdir -p $(CUR_BUILD_DIR)
|
||||
( cd $(CUR_BUILD_DIR); rm -f config.cache; \
|
||||
$(GLIBC_CONFIGURE) \
|
||||
|
@@ -1303,19 +1303,21 @@ local function processData(szType, content, add_mode, add_from)
|
||||
end
|
||||
|
||||
local function curl(url, file, ua, mode)
|
||||
local curl_args = api.clone(api.curl_args)
|
||||
local curl_args = {
|
||||
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3"
|
||||
}
|
||||
if ua and ua ~= "" and ua ~= "curl" then
|
||||
table.insert(curl_args, '--user-agent "' .. ua .. '"')
|
||||
curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"'
|
||||
end
|
||||
local return_code
|
||||
local return_code, result
|
||||
if mode == "direct" then
|
||||
return_code = api.curl_direct(url, file, curl_args)
|
||||
return_code, result = api.curl_direct(url, file, curl_args)
|
||||
elseif mode == "proxy" then
|
||||
return_code = api.curl_proxy(url, file, curl_args)
|
||||
return_code, result = api.curl_proxy(url, file, curl_args)
|
||||
else
|
||||
return_code = api.curl_auto(url, file, curl_args)
|
||||
return_code, result = api.curl_auto(url, file, curl_args)
|
||||
end
|
||||
return return_code
|
||||
return tonumber(result)
|
||||
end
|
||||
|
||||
local function truncate_nodes(add_from)
|
||||
@@ -1714,34 +1716,23 @@ local execute = function()
|
||||
local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连访问" or (access_mode == "proxy" and "通过代理" or "自动"))
|
||||
log('正在订阅:【' .. remark .. '】' .. url .. ' [' .. result .. ']')
|
||||
local tmp_file = "/tmp/" .. cfgid
|
||||
local raw = curl(url, tmp_file, ua, access_mode)
|
||||
if raw ~= 0 then
|
||||
value.http_code = curl(url, tmp_file, ua, access_mode)
|
||||
if value.http_code ~= 200 then
|
||||
fail_list[#fail_list + 1] = value
|
||||
else
|
||||
if luci.sys.call("[ -f " .. tmp_file .. " ] && sed -i -e '/^[ \t]*$/d' -e '/^[ \t]*\r$/d' " .. tmp_file) == 0 then
|
||||
local f = io.open(tmp_file, "r")
|
||||
local count = 0
|
||||
for _ in f:lines() do
|
||||
count = count + 1
|
||||
end
|
||||
if count == 1 then
|
||||
f:seek("set")
|
||||
local stdout = f:read("*all")
|
||||
f:close()
|
||||
local raw_data = trim(stdout)
|
||||
local old_md5 = value.md5 or ""
|
||||
local new_md5 = luci.sys.exec("md5sum " .. tmp_file .. " 2>/dev/null | awk '{print $1}'"):gsub("\n", "")
|
||||
os.remove(tmp_file)
|
||||
if old_md5 == new_md5 then
|
||||
log('订阅:【' .. remark .. '】没有变化,无需更新。')
|
||||
else
|
||||
parse_link(raw_data, "2", remark, cfgid)
|
||||
uci:set(appname, cfgid, "md5", new_md5)
|
||||
end
|
||||
local stdout = f:read("*all")
|
||||
f:close()
|
||||
local raw_data = trim(stdout)
|
||||
local old_md5 = value.md5 or ""
|
||||
local new_md5 = luci.sys.exec("md5sum " .. tmp_file .. " 2>/dev/null | awk '{print $1}'"):gsub("\n", "")
|
||||
os.remove(tmp_file)
|
||||
if old_md5 == new_md5 then
|
||||
log('订阅:【' .. remark .. '】没有变化,无需更新。')
|
||||
else
|
||||
f:close()
|
||||
os.remove(tmp_file)
|
||||
fail_list[#fail_list + 1] = value
|
||||
parse_link(raw_data, "2", remark, cfgid)
|
||||
uci:set(appname, cfgid, "md5", new_md5)
|
||||
end
|
||||
else
|
||||
fail_list[#fail_list + 1] = value
|
||||
@@ -1760,7 +1751,7 @@ local execute = function()
|
||||
|
||||
if #fail_list > 0 then
|
||||
for index, value in ipairs(fail_list) do
|
||||
log(string.format('【%s】订阅失败,可能是订阅地址无效,或是网络问题,请诊断!', value.remark))
|
||||
log(string.format('【%s】订阅失败,可能是订阅地址无效,或是网络问题,请诊断![%s]', value.remark, tostring(value.http_code)))
|
||||
end
|
||||
end
|
||||
update_node(0)
|
||||
|
@@ -1303,19 +1303,21 @@ local function processData(szType, content, add_mode, add_from)
|
||||
end
|
||||
|
||||
local function curl(url, file, ua, mode)
|
||||
local curl_args = api.clone(api.curl_args)
|
||||
local curl_args = {
|
||||
"-skL", "-w %{http_code}", "--retry 3", "--connect-timeout 3"
|
||||
}
|
||||
if ua and ua ~= "" and ua ~= "curl" then
|
||||
table.insert(curl_args, '--user-agent "' .. ua .. '"')
|
||||
curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"'
|
||||
end
|
||||
local return_code
|
||||
local return_code, result
|
||||
if mode == "direct" then
|
||||
return_code = api.curl_direct(url, file, curl_args)
|
||||
return_code, result = api.curl_direct(url, file, curl_args)
|
||||
elseif mode == "proxy" then
|
||||
return_code = api.curl_proxy(url, file, curl_args)
|
||||
return_code, result = api.curl_proxy(url, file, curl_args)
|
||||
else
|
||||
return_code = api.curl_auto(url, file, curl_args)
|
||||
return_code, result = api.curl_auto(url, file, curl_args)
|
||||
end
|
||||
return return_code
|
||||
return tonumber(result)
|
||||
end
|
||||
|
||||
local function truncate_nodes(add_from)
|
||||
@@ -1714,34 +1716,23 @@ local execute = function()
|
||||
local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连访问" or (access_mode == "proxy" and "通过代理" or "自动"))
|
||||
log('正在订阅:【' .. remark .. '】' .. url .. ' [' .. result .. ']')
|
||||
local tmp_file = "/tmp/" .. cfgid
|
||||
local raw = curl(url, tmp_file, ua, access_mode)
|
||||
if raw ~= 0 then
|
||||
value.http_code = curl(url, tmp_file, ua, access_mode)
|
||||
if value.http_code ~= 200 then
|
||||
fail_list[#fail_list + 1] = value
|
||||
else
|
||||
if luci.sys.call("[ -f " .. tmp_file .. " ] && sed -i -e '/^[ \t]*$/d' -e '/^[ \t]*\r$/d' " .. tmp_file) == 0 then
|
||||
local f = io.open(tmp_file, "r")
|
||||
local count = 0
|
||||
for _ in f:lines() do
|
||||
count = count + 1
|
||||
end
|
||||
if count == 1 then
|
||||
f:seek("set")
|
||||
local stdout = f:read("*all")
|
||||
f:close()
|
||||
local raw_data = trim(stdout)
|
||||
local old_md5 = value.md5 or ""
|
||||
local new_md5 = luci.sys.exec("md5sum " .. tmp_file .. " 2>/dev/null | awk '{print $1}'"):gsub("\n", "")
|
||||
os.remove(tmp_file)
|
||||
if old_md5 == new_md5 then
|
||||
log('订阅:【' .. remark .. '】没有变化,无需更新。')
|
||||
else
|
||||
parse_link(raw_data, "2", remark, cfgid)
|
||||
uci:set(appname, cfgid, "md5", new_md5)
|
||||
end
|
||||
local stdout = f:read("*all")
|
||||
f:close()
|
||||
local raw_data = trim(stdout)
|
||||
local old_md5 = value.md5 or ""
|
||||
local new_md5 = luci.sys.exec("md5sum " .. tmp_file .. " 2>/dev/null | awk '{print $1}'"):gsub("\n", "")
|
||||
os.remove(tmp_file)
|
||||
if old_md5 == new_md5 then
|
||||
log('订阅:【' .. remark .. '】没有变化,无需更新。')
|
||||
else
|
||||
f:close()
|
||||
os.remove(tmp_file)
|
||||
fail_list[#fail_list + 1] = value
|
||||
parse_link(raw_data, "2", remark, cfgid)
|
||||
uci:set(appname, cfgid, "md5", new_md5)
|
||||
end
|
||||
else
|
||||
fail_list[#fail_list + 1] = value
|
||||
@@ -1760,7 +1751,7 @@ local execute = function()
|
||||
|
||||
if #fail_list > 0 then
|
||||
for index, value in ipairs(fail_list) do
|
||||
log(string.format('【%s】订阅失败,可能是订阅地址无效,或是网络问题,请诊断!', value.remark))
|
||||
log(string.format('【%s】订阅失败,可能是订阅地址无效,或是网络问题,请诊断![%s]', value.remark, tostring(value.http_code)))
|
||||
end
|
||||
end
|
||||
update_node(0)
|
||||
|
@@ -326,10 +326,10 @@ update_subscription() {
|
||||
if (curl -s -f --connect-timeout 15 --retry 3 -L -X GET -A "$subscription_user_agent" -D "$subscription_header_tmpfile" -o "$subscription_tmpfile" "$subscription_url"); then
|
||||
log "Profile" "Subscription update successful."
|
||||
local subscription_expire subscription_upload subscription_download subscription_total subscription_used subscription_avaliable
|
||||
subscription_expire=$(grep "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "expire=[[:digit:]]+" | cut -d '=' -f 2)
|
||||
subscription_upload=$(grep "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "upload=[[:digit:]]+" | cut -d '=' -f 2)
|
||||
subscription_download=$(grep "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "download=[[:digit:]]+" | cut -d '=' -f 2)
|
||||
subscription_total=$(grep "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "total=[[:digit:]]+" | cut -d '=' -f 2)
|
||||
subscription_expire=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "expire=[[:digit:]]+" | cut -d '=' -f 2)
|
||||
subscription_upload=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "upload=[[:digit:]]+" | cut -d '=' -f 2)
|
||||
subscription_download=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "download=[[:digit:]]+" | cut -d '=' -f 2)
|
||||
subscription_total=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -o -E "total=[[:digit:]]+" | cut -d '=' -f 2)
|
||||
if [[ -n "$subscription_upload" && -n "$subscription_download" ]]; then
|
||||
subscription_used=$((subscription_upload + subscription_download))
|
||||
if [ -n "$subscription_total" ]; then
|
||||
|
@@ -1,12 +1,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=geoview
|
||||
PKG_VERSION:=0.1.3
|
||||
PKG_VERSION:=0.1.4
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/snowie2000/geoview/tar.gz/$(PKG_VERSION)?
|
||||
PKG_HASH:=6f7b9f80105f665d39cdbd0122f7aeadbaf6d402e10d9e66d0c8de99b8e078f7
|
||||
PKG_HASH:=27dee73635fd77095435354506da15ec5a3dbd999fad7ccfd1d322a46ad16c84
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
@@ -11,14 +11,18 @@ class LogcatRecyclerAdapter(val activity: LogcatActivity) : RecyclerView.Adapter
|
||||
override fun getItemCount() = mActivity.logsets.size
|
||||
|
||||
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
|
||||
val log = mActivity.logsets[position]
|
||||
if (log.isEmpty()) {
|
||||
holder.itemSubSettingBinding.logTag.text = ""
|
||||
holder.itemSubSettingBinding.logContent.text = ""
|
||||
} else {
|
||||
val content = log.split("):", limit = 2)
|
||||
holder.itemSubSettingBinding.logTag.text = content.first().trim()
|
||||
holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else ""
|
||||
try {
|
||||
val log = mActivity.logsets[position]
|
||||
if (log.isEmpty()) {
|
||||
holder.itemSubSettingBinding.logTag.text = ""
|
||||
holder.itemSubSettingBinding.logContent.text = ""
|
||||
} else {
|
||||
val content = log.split("):", limit = 2)
|
||||
holder.itemSubSettingBinding.logTag.text = content.first().split("(", limit = 2).first().trim()
|
||||
holder.itemSubSettingBinding.logContent.text = if (content.count() > 1) content.last().trim() else ""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -69,7 +69,11 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
||||
} else {
|
||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
||||
}
|
||||
holder.itemMainBinding.tvSubscription.text = MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks ?: ""
|
||||
holder.itemMainBinding.tvSubscription.text =
|
||||
if (mActivity.mainViewModel.subscriptionId.isEmpty())
|
||||
MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks.orEmpty()
|
||||
else
|
||||
""
|
||||
|
||||
var shareOptions = share_method.asList()
|
||||
when (profile.configType) {
|
||||
|
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
|
@@ -4,7 +4,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
|
@@ -4,7 +4,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.LogcatActivity">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
|
@@ -4,7 +4,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.SubSettingActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -4,7 +4,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.RoutingSettingActivity">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.ServerCustomConfigActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.ServerActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.ServerActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.ServerActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.ServerActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.ServerActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.ServerActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.ServerActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical"
|
||||
tools:context=".ui.SettingsActivity">
|
||||
|
||||
|
@@ -4,7 +4,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.SubSettingActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -4,7 +4,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.SubSettingActivity">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp16"
|
||||
tools:context=".ui.TaskerActivity">
|
||||
|
@@ -3,7 +3,6 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.UserAssetUrlActivity">
|
||||
|
||||
<LinearLayout
|
||||
|
@@ -47,18 +47,13 @@
|
||||
android:minLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_statistics"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textSize="12sp" />
|
||||
android:paddingTop="@dimen/margin_spacing_dp8"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -74,7 +69,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingEnd="@dimen/padding_spacing_dp16">
|
||||
|
||||
<TextView
|
||||
@@ -85,7 +80,7 @@
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="@color/color_secondary"
|
||||
android:textSize="10sp"
|
||||
android:textSize="11sp"
|
||||
tools:text="Sub" />
|
||||
|
||||
<TextView
|
||||
@@ -95,7 +90,7 @@
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="@color/colorPing"
|
||||
android:textSize="10sp"
|
||||
android:textSize="11sp"
|
||||
tools:text="214ms" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -167,7 +162,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="right"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="4dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:paddingEnd="@dimen/padding_spacing_dp16">
|
||||
|
||||
<TextView
|
||||
@@ -177,7 +172,7 @@
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="@color/colorConfigType"
|
||||
android:textSize="10sp" />
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@@ -46,7 +46,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textSize="12sp"
|
||||
tools:text="1MB . 2020.01.01" />
|
||||
|
||||
|
||||
|
@@ -27,5 +27,5 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="10sp" />
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
</LinearLayout>
|
||||
|
@@ -8,10 +8,6 @@
|
||||
android:id="@+id/sub_setting"
|
||||
android:icon="@drawable/ic_subscriptions_24dp"
|
||||
android:title="@string/title_sub_setting" />
|
||||
<item
|
||||
android:id="@+id/settings"
|
||||
android:icon="@drawable/ic_settings_24dp"
|
||||
android:title="@string/title_settings" />
|
||||
<item
|
||||
android:id="@+id/per_app_proxy_settings"
|
||||
android:icon="@drawable/ic_per_apps_24dp"
|
||||
@@ -20,6 +16,10 @@
|
||||
android:id="@+id/routing_setting"
|
||||
android:icon="@drawable/ic_routing_24dp"
|
||||
android:title="@string/routing_settings_title" />
|
||||
<item
|
||||
android:id="@+id/settings"
|
||||
android:icon="@drawable/ic_settings_24dp"
|
||||
android:title="@string/title_settings" />
|
||||
</group>
|
||||
|
||||
<group android:id="@+id/group_id2">
|
||||
|
@@ -5,8 +5,9 @@
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:statusBarColor">@color/colorPrimary</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimary</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="colorControlNormal">@color/colorAccent</item>
|
||||
<item name="android:fitsSystemWindows">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
@@ -4,8 +4,9 @@
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:statusBarColor">@color/colorPrimary</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimary</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="colorControlNormal">@color/colorAccent</item>
|
||||
<item name="android:fitsSystemWindows">true</item>
|
||||
</style>
|
||||
|
||||
<style name="AppThemeDayNight.NoActionBar" parent="AppThemeDayNight">
|
||||
|
@@ -221,7 +221,7 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP n
|
||||
|
||||
// As we don't want our traffic pattern looks like DoH, we use Random-Length Padding instead of Block-Length Padding recommended in RFC 8467
|
||||
// Although DoH server like 1.1.1.1 will pad the response to Block-Length 468, at least it is better than no padding for response at all
|
||||
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP, int(crypto.RandBetween(100, 600))))
|
||||
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP, int(crypto.RandBetween(100, 300))))
|
||||
|
||||
var deadline time.Time
|
||||
if d, ok := ctx.Deadline(); ok {
|
||||
|
@@ -742,3 +742,18 @@ lfavole
|
||||
mp3butcher
|
||||
slipinthedove
|
||||
YoshiTabletopGamer
|
||||
Arc8ne
|
||||
benfaerber
|
||||
chrisellsworth
|
||||
fries1234
|
||||
Kenshin9977
|
||||
MichaelDeBoey
|
||||
msikma
|
||||
pedro
|
||||
pferreir
|
||||
red-acid
|
||||
refack
|
||||
rysson
|
||||
somini
|
||||
thedenv
|
||||
vallovic
|
||||
|
@@ -4,6 +4,79 @@
|
||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||
-->
|
||||
|
||||
### 2025.03.21
|
||||
|
||||
#### Core changes
|
||||
- [Fix external downloader availability when using `--ffmpeg-location`](https://github.com/yt-dlp/yt-dlp/commit/9f77e04c76e36e1cbbf49bc9eb385fa6ef804b67) ([#12318](https://github.com/yt-dlp/yt-dlp/issues/12318)) by [Kenshin9977](https://github.com/Kenshin9977)
|
||||
- [Load plugins on demand](https://github.com/yt-dlp/yt-dlp/commit/4445f37a7a66b248dbd8376c43137e6e441f138e) ([#11305](https://github.com/yt-dlp/yt-dlp/issues/11305)) by [coletdjnz](https://github.com/coletdjnz), [Grub4K](https://github.com/Grub4K), [pukkandan](https://github.com/pukkandan) (With fixes in [c034d65](https://github.com/yt-dlp/yt-dlp/commit/c034d655487be668222ef9476a16f374584e49a7))
|
||||
- [Support emitting ConEmu progress codes](https://github.com/yt-dlp/yt-dlp/commit/f7a1f2d8132967a62b0f6d5665c6d2dde2d42c09) ([#10649](https://github.com/yt-dlp/yt-dlp/issues/10649)) by [Grub4K](https://github.com/Grub4K)
|
||||
|
||||
#### Extractor changes
|
||||
- **azmedien**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/26a502fc727d0e91b2db6bf4a112823bcc672e85) ([#12375](https://github.com/yt-dlp/yt-dlp/issues/12375)) by [goggle](https://github.com/goggle)
|
||||
- **bilibiliplaylist**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f5fb2229e66cf59d5bf16065bc041b42a28354a0) ([#12690](https://github.com/yt-dlp/yt-dlp/issues/12690)) by [bashonly](https://github.com/bashonly)
|
||||
- **bunnycdn**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/3a1583ca75fb523cbad0e5e174387ea7b477d175) ([#11586](https://github.com/yt-dlp/yt-dlp/issues/11586)) by [Grub4K](https://github.com/Grub4K), [seproDev](https://github.com/seproDev)
|
||||
- **canalsurmas**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/01a8be4c23f186329d85f9c78db34a55f3294ac5) ([#12497](https://github.com/yt-dlp/yt-dlp/issues/12497)) by [Arc8ne](https://github.com/Arc8ne)
|
||||
- **cda**: [Fix login support](https://github.com/yt-dlp/yt-dlp/commit/be0d819e1103195043f6743650781f0d4d343f6d) ([#12552](https://github.com/yt-dlp/yt-dlp/issues/12552)) by [rysson](https://github.com/rysson)
|
||||
- **cultureunplugged**: [Extend `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/3042afb5fe342d3a00de76704cd7de611acc350e) ([#12486](https://github.com/yt-dlp/yt-dlp/issues/12486)) by [seproDev](https://github.com/seproDev)
|
||||
- **dailymotion**: [Improve embed detection](https://github.com/yt-dlp/yt-dlp/commit/ad60137c141efa5023fbc0ac8579eaefe8b3d8cc) ([#12464](https://github.com/yt-dlp/yt-dlp/issues/12464)) by [seproDev](https://github.com/seproDev)
|
||||
- **gem.cbc.ca**: [Fix login support](https://github.com/yt-dlp/yt-dlp/commit/eb1417786a3027b1e7290ec37ef6aaece50ebed0) ([#12414](https://github.com/yt-dlp/yt-dlp/issues/12414)) by [bashonly](https://github.com/bashonly)
|
||||
- **globo**: [Fix subtitles extraction](https://github.com/yt-dlp/yt-dlp/commit/0e1697232fcbba7551f983fd1ba93bb445cbb08b) ([#12270](https://github.com/yt-dlp/yt-dlp/issues/12270)) by [pedro](https://github.com/pedro)
|
||||
- **instagram**
|
||||
- [Add `app_id` extractor-arg](https://github.com/yt-dlp/yt-dlp/commit/a90641c8363fa0c10800b36eb6b01ee22d3a9409) ([#12359](https://github.com/yt-dlp/yt-dlp/issues/12359)) by [chrisellsworth](https://github.com/chrisellsworth)
|
||||
- [Fix extraction of older private posts](https://github.com/yt-dlp/yt-dlp/commit/a59abe0636dc49b22a67246afe35613571b86f05) ([#12451](https://github.com/yt-dlp/yt-dlp/issues/12451)) by [bashonly](https://github.com/bashonly)
|
||||
- [Improve error handling](https://github.com/yt-dlp/yt-dlp/commit/480125560a3b9972d29ae0da850aba8109e6bd41) ([#12410](https://github.com/yt-dlp/yt-dlp/issues/12410)) by [bashonly](https://github.com/bashonly)
|
||||
- story: [Support `--no-playlist`](https://github.com/yt-dlp/yt-dlp/commit/65c3c58c0a67463a150920203cec929045c95a24) ([#12397](https://github.com/yt-dlp/yt-dlp/issues/12397)) by [fireattack](https://github.com/fireattack)
|
||||
- **jamendo**: [Fix thumbnail extraction](https://github.com/yt-dlp/yt-dlp/commit/89a68c4857ddbaf937ff22f12648baaf6b5af840) ([#12622](https://github.com/yt-dlp/yt-dlp/issues/12622)) by [bashonly](https://github.com/bashonly), [JChris246](https://github.com/JChris246)
|
||||
- **ketnet**: [Remove extractor](https://github.com/yt-dlp/yt-dlp/commit/bbada3ec0779422cde34f1ce3dcf595da463b493) ([#12628](https://github.com/yt-dlp/yt-dlp/issues/12628)) by [MichaelDeBoey](https://github.com/MichaelDeBoey)
|
||||
- **lbry**
|
||||
- [Make m3u8 format extraction non-fatal](https://github.com/yt-dlp/yt-dlp/commit/9807181cfbf87bfa732f415c30412bdbd77cbf81) ([#12463](https://github.com/yt-dlp/yt-dlp/issues/12463)) by [bashonly](https://github.com/bashonly)
|
||||
- [Raise appropriate error for non-media files](https://github.com/yt-dlp/yt-dlp/commit/7126b472601814b7fd8c9de02069e8fff1764891) ([#12462](https://github.com/yt-dlp/yt-dlp/issues/12462)) by [bashonly](https://github.com/bashonly)
|
||||
- **loco**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/983095485c731240aae27c950cb8c24a50827b56) ([#12667](https://github.com/yt-dlp/yt-dlp/issues/12667)) by [DTrombett](https://github.com/DTrombett)
|
||||
- **magellantv**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/172d5fcd778bf2605db7647ebc56b29ed18d24ac) ([#12505](https://github.com/yt-dlp/yt-dlp/issues/12505)) by [seproDev](https://github.com/seproDev)
|
||||
- **mitele**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/7223d29569a48a35ad132a508c115973866838d3) ([#12689](https://github.com/yt-dlp/yt-dlp/issues/12689)) by [bashonly](https://github.com/bashonly)
|
||||
- **msn**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/4815dac131d42c51e12c1d05232db0bbbf607329) ([#12513](https://github.com/yt-dlp/yt-dlp/issues/12513)) by [seproDev](https://github.com/seproDev), [thedenv](https://github.com/thedenv)
|
||||
- **n1**: [Fix extraction of newer articles](https://github.com/yt-dlp/yt-dlp/commit/9d70abe4de401175cbbaaa36017806f16b2df9af) ([#12514](https://github.com/yt-dlp/yt-dlp/issues/12514)) by [u-spec-png](https://github.com/u-spec-png)
|
||||
- **nbcstations**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/ebac65aa9e0bf9a97c24d00f7977900d2577364b) ([#12534](https://github.com/yt-dlp/yt-dlp/issues/12534)) by [refack](https://github.com/refack)
|
||||
- **niconico**
|
||||
- [Fix format sorting](https://github.com/yt-dlp/yt-dlp/commit/7508e34f203e97389f1d04db92140b13401dd724) ([#12442](https://github.com/yt-dlp/yt-dlp/issues/12442)) by [xpadev-net](https://github.com/xpadev-net)
|
||||
- live: [Fix thumbnail extraction](https://github.com/yt-dlp/yt-dlp/commit/c2e6e1d5f77f3b720a6266f2869eb750d20e5dc1) ([#12419](https://github.com/yt-dlp/yt-dlp/issues/12419)) by [bashonly](https://github.com/bashonly)
|
||||
- **openrec**: [Fix `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/17504f253564cfad86244de2b6346d07d2300ca5) ([#12608](https://github.com/yt-dlp/yt-dlp/issues/12608)) by [fireattack](https://github.com/fireattack)
|
||||
- **pinterest**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/bd0a66816934de70312eea1e71c59c13b401dc3a) ([#12538](https://github.com/yt-dlp/yt-dlp/issues/12538)) by [mikf](https://github.com/mikf)
|
||||
- **playsuisse**: [Fix login support](https://github.com/yt-dlp/yt-dlp/commit/6933f5670cea9c3e2fb16c1caa1eda54d13122c5) ([#12444](https://github.com/yt-dlp/yt-dlp/issues/12444)) by [bashonly](https://github.com/bashonly)
|
||||
- **reddit**: [Truncate title](https://github.com/yt-dlp/yt-dlp/commit/d9a53cc1e6fd912daf500ca4f19e9ca88994dbf9) ([#12567](https://github.com/yt-dlp/yt-dlp/issues/12567)) by [seproDev](https://github.com/seproDev)
|
||||
- **rtp**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/8eb9c1bf3b9908cca22ef043602aa24fb9f352c6) ([#11638](https://github.com/yt-dlp/yt-dlp/issues/11638)) by [pferreir](https://github.com/pferreir), [red-acid](https://github.com/red-acid), [seproDev](https://github.com/seproDev), [somini](https://github.com/somini), [vallovic](https://github.com/vallovic)
|
||||
- **softwhiteunderbelly**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/652827d5a076c9483c36654ad2cf3fe46219baf4) ([#12281](https://github.com/yt-dlp/yt-dlp/issues/12281)) by [benfaerber](https://github.com/benfaerber)
|
||||
- **soop**: [Fix timestamp extraction](https://github.com/yt-dlp/yt-dlp/commit/8305df00012ff8138a6ff95279d06b54ac607f63) ([#12609](https://github.com/yt-dlp/yt-dlp/issues/12609)) by [msikma](https://github.com/msikma)
|
||||
- **soundcloud**
|
||||
- [Extract tags](https://github.com/yt-dlp/yt-dlp/commit/9deed13d7cce6d3647379e50589c92de89227509) ([#12420](https://github.com/yt-dlp/yt-dlp/issues/12420)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix thumbnail extraction](https://github.com/yt-dlp/yt-dlp/commit/6deeda5c11f34f613724fa0627879f0d607ba1b4) ([#12447](https://github.com/yt-dlp/yt-dlp/issues/12447)) by [bashonly](https://github.com/bashonly)
|
||||
- **tiktok**
|
||||
- [Improve error handling](https://github.com/yt-dlp/yt-dlp/commit/99ea2978757a431eeb2a265b3395ccbe4ce202cf) ([#12445](https://github.com/yt-dlp/yt-dlp/issues/12445)) by [bashonly](https://github.com/bashonly)
|
||||
- [Truncate title](https://github.com/yt-dlp/yt-dlp/commit/83b119dadb0f267f1fb66bf7ed74c097349de79e) ([#12566](https://github.com/yt-dlp/yt-dlp/issues/12566)) by [seproDev](https://github.com/seproDev)
|
||||
- **tv8.it**: [Add live and playlist extractors](https://github.com/yt-dlp/yt-dlp/commit/2ee3a0aff9be2be3bea60640d3d8a0febaf0acb6) ([#12569](https://github.com/yt-dlp/yt-dlp/issues/12569)) by [DTrombett](https://github.com/DTrombett)
|
||||
- **tvw**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/42b7440963866e31ff84a5b89030d1c596fa2e6e) ([#12271](https://github.com/yt-dlp/yt-dlp/issues/12271)) by [fries1234](https://github.com/fries1234)
|
||||
- **twitter**
|
||||
- [Fix syndication token generation](https://github.com/yt-dlp/yt-dlp/commit/b8b47547049f5ebc3dd680fc7de70ed0ca9c0d70) ([#12537](https://github.com/yt-dlp/yt-dlp/issues/12537)) by [bashonly](https://github.com/bashonly)
|
||||
- [Truncate title](https://github.com/yt-dlp/yt-dlp/commit/06f6de78db2eceeabd062ab1a3023e0ff9d4df53) ([#12560](https://github.com/yt-dlp/yt-dlp/issues/12560)) by [seproDev](https://github.com/seproDev)
|
||||
- **vk**: [Improve metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/05c8023a27dd37c49163c0498bf98e3e3c1cb4b9) ([#12510](https://github.com/yt-dlp/yt-dlp/issues/12510)) by [seproDev](https://github.com/seproDev)
|
||||
- **vrtmax**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/df9ebeec00d658693252978d1ffb885e67aa6ab6) ([#12479](https://github.com/yt-dlp/yt-dlp/issues/12479)) by [bergoid](https://github.com/bergoid), [MichaelDeBoey](https://github.com/MichaelDeBoey), [seproDev](https://github.com/seproDev)
|
||||
- **weibo**: [Support playlists](https://github.com/yt-dlp/yt-dlp/commit/0bb39788626002a8a67e925580227952c563c8b9) ([#12284](https://github.com/yt-dlp/yt-dlp/issues/12284)) by [4ft35t](https://github.com/4ft35t)
|
||||
- **wsj**: [Support opinion URLs and impersonation](https://github.com/yt-dlp/yt-dlp/commit/7f3006eb0c0659982bb956d71b0bc806bcb0a5f2) ([#12431](https://github.com/yt-dlp/yt-dlp/issues/12431)) by [refack](https://github.com/refack)
|
||||
- **youtube**
|
||||
- [Fix nsig and signature extraction for player `643afba4`](https://github.com/yt-dlp/yt-dlp/commit/9b868518a15599f3d7ef5a1c730dda164c30da9b) ([#12684](https://github.com/yt-dlp/yt-dlp/issues/12684)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||
- [Player client maintenance](https://github.com/yt-dlp/yt-dlp/commit/3380febe9984c21c79c3147c1d390a4cf339bc4c) ([#12603](https://github.com/yt-dlp/yt-dlp/issues/12603)) by [seproDev](https://github.com/seproDev)
|
||||
- [Split into package](https://github.com/yt-dlp/yt-dlp/commit/4432a9390c79253ac830702b226d2e558b636725) ([#12557](https://github.com/yt-dlp/yt-dlp/issues/12557)) by [coletdjnz](https://github.com/coletdjnz)
|
||||
- [Warn on DRM formats](https://github.com/yt-dlp/yt-dlp/commit/e67d786c7cc87bd449d22e0ddef08306891c1173) ([#12593](https://github.com/yt-dlp/yt-dlp/issues/12593)) by [coletdjnz](https://github.com/coletdjnz)
|
||||
- [Warn on missing formats due to SSAP](https://github.com/yt-dlp/yt-dlp/commit/79ec2fdff75c8c1bb89b550266849ad4dec48dd3) ([#12483](https://github.com/yt-dlp/yt-dlp/issues/12483)) by [coletdjnz](https://github.com/coletdjnz)
|
||||
|
||||
#### Networking changes
|
||||
- [Add `keep_header_casing` extension](https://github.com/yt-dlp/yt-dlp/commit/7d18fed8f1983fe6de4ddc810dfb2761ba5744ac) ([#11652](https://github.com/yt-dlp/yt-dlp/issues/11652)) by [coletdjnz](https://github.com/coletdjnz), [Grub4K](https://github.com/Grub4K)
|
||||
- [Always add unsupported suffix on version mismatch](https://github.com/yt-dlp/yt-dlp/commit/95f8df2f796d0048119615200758199aedcd7cf4) ([#12626](https://github.com/yt-dlp/yt-dlp/issues/12626)) by [Grub4K](https://github.com/Grub4K)
|
||||
|
||||
#### Misc. changes
|
||||
- **cleanup**: Miscellaneous: [f36e4b6](https://github.com/yt-dlp/yt-dlp/commit/f36e4b6e65cb8403791aae2f520697115cb88dec) by [dirkf](https://github.com/dirkf), [gamer191](https://github.com/gamer191), [Grub4K](https://github.com/Grub4K), [seproDev](https://github.com/seproDev)
|
||||
- **test**: [Show all differences for `expect_value` and `expect_dict`](https://github.com/yt-dlp/yt-dlp/commit/a3e0c7d3b267abdf3933b709704a28d43bb46503) ([#12334](https://github.com/yt-dlp/yt-dlp/issues/12334)) by [Grub4K](https://github.com/Grub4K)
|
||||
|
||||
### 2025.02.19
|
||||
|
||||
#### Core changes
|
||||
|
@@ -76,7 +76,7 @@ dev = [
|
||||
]
|
||||
static-analysis = [
|
||||
"autopep8~=2.0",
|
||||
"ruff~=0.9.0",
|
||||
"ruff~=0.11.0",
|
||||
]
|
||||
test = [
|
||||
"pytest~=8.1",
|
||||
@@ -387,7 +387,11 @@ select = [
|
||||
exclude = "*/extractor/lazy_extractors.py,*venv*,*/test/testdata/sigs/player-*.js,.idea,.vscode"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-ra -v --strict-markers"
|
||||
addopts = [
|
||||
"-ra", # summary: all except passed
|
||||
"--verbose",
|
||||
"--strict-markers",
|
||||
]
|
||||
markers = [
|
||||
"download",
|
||||
]
|
||||
|
@@ -224,6 +224,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **bt:vestlendingen**: Bergens Tidende - Vestlendingen
|
||||
- **Bundesliga**
|
||||
- **Bundestag**
|
||||
- **BunnyCdn**
|
||||
- **BusinessInsider**
|
||||
- **BuzzFeed**
|
||||
- **BYUtv**: (**Currently broken**)
|
||||
@@ -242,6 +243,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **CanalAlpha**
|
||||
- **canalc2.tv**
|
||||
- **Canalplus**: mycanal.fr and piwiplus.fr
|
||||
- **Canalsurmas**
|
||||
- **CaracolTvPlay**: [*caracoltv-play*](## "netrc machine")
|
||||
- **CartoonNetwork**
|
||||
- **cbc.ca**
|
||||
@@ -609,10 +611,10 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Inc**
|
||||
- **IndavideoEmbed**
|
||||
- **InfoQ**
|
||||
- **Instagram**: [*instagram*](## "netrc machine")
|
||||
- **instagram:story**: [*instagram*](## "netrc machine")
|
||||
- **instagram:tag**: [*instagram*](## "netrc machine") Instagram hashtag search URLs
|
||||
- **instagram:user**: [*instagram*](## "netrc machine") Instagram user profile (**Currently broken**)
|
||||
- **Instagram**
|
||||
- **instagram:story**
|
||||
- **instagram:tag**: Instagram hashtag search URLs
|
||||
- **instagram:user**: Instagram user profile (**Currently broken**)
|
||||
- **InstagramIOS**: IOS instagram:// URL
|
||||
- **Internazionale**
|
||||
- **InternetVideoArchive**
|
||||
@@ -661,7 +663,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **KelbyOne**: (**Currently broken**)
|
||||
- **Kenh14Playlist**
|
||||
- **Kenh14Video**
|
||||
- **Ketnet**
|
||||
- **khanacademy**
|
||||
- **khanacademy:unit**
|
||||
- **kick:clips**
|
||||
@@ -733,6 +734,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Livestreamfails**
|
||||
- **Lnk**
|
||||
- **loc**: Library of Congress
|
||||
- **Loco**
|
||||
- **loom**
|
||||
- **loom:folder**
|
||||
- **LoveHomePorn**
|
||||
@@ -831,7 +833,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **MoviewPlay**
|
||||
- **Moviezine**
|
||||
- **MovingImage**
|
||||
- **MSN**: (**Currently broken**)
|
||||
- **MSN**
|
||||
- **mtg**: MTG services
|
||||
- **mtv**
|
||||
- **mtv.de**: (**Currently broken**)
|
||||
@@ -1342,6 +1344,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Smotrim**
|
||||
- **SnapchatSpotlight**
|
||||
- **Snotr**
|
||||
- **SoftWhiteUnderbelly**: [*softwhiteunderbelly*](## "netrc machine")
|
||||
- **Sohu**
|
||||
- **SohuV**
|
||||
- **SonyLIV**: [*sonyliv*](## "netrc machine")
|
||||
@@ -1536,6 +1539,8 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **tv5unis**
|
||||
- **tv5unis:video**
|
||||
- **tv8.it**
|
||||
- **tv8.it:live**: TV8 Live
|
||||
- **tv8.it:playlist**: TV8 Playlist
|
||||
- **TVANouvelles**
|
||||
- **TVANouvellesArticle**
|
||||
- **tvaplus**: TVA+
|
||||
@@ -1556,6 +1561,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **tvp:vod:series**
|
||||
- **TVPlayer**
|
||||
- **TVPlayHome**
|
||||
- **Tvw**
|
||||
- **Tweakers**
|
||||
- **TwitCasting**
|
||||
- **TwitCastingLive**
|
||||
@@ -1677,7 +1683,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **vqq:series**
|
||||
- **vqq:video**
|
||||
- **VRT**: VRT NWS, Flanders News, Flandern Info and Sporza
|
||||
- **VrtNU**: [*vrtnu*](## "netrc machine") VRT MAX
|
||||
- **vrtmax**: [*vrtnu*](## "netrc machine") VRT MAX (formerly VRT NU)
|
||||
- **VTM**: (**Currently broken**)
|
||||
- **VTV**
|
||||
- **VTVGo**
|
||||
|
@@ -331,10 +331,6 @@ class TestHTTPConnectProxy:
|
||||
assert proxy_info['proxy'] == server_address
|
||||
assert 'Proxy-Authorization' in proxy_info['headers']
|
||||
|
||||
@pytest.mark.skip_handler(
|
||||
'Requests',
|
||||
'bug in urllib3 causes unclosed socket: https://github.com/urllib3/urllib3/issues/3374',
|
||||
)
|
||||
def test_http_connect_bad_auth(self, handler, ctx):
|
||||
with ctx.http_server(HTTPConnectProxyHandler, username='test', password='test') as server_address:
|
||||
with handler(verify=False, proxies={ctx.REQUEST_PROTO: f'http://test:bad@{server_address}'}) as rh:
|
||||
|
@@ -384,7 +384,7 @@ class TestJSInterpreter(unittest.TestCase):
|
||||
@unittest.skip('Not implemented')
|
||||
def test_packed(self):
|
||||
jsi = JSInterpreter('''function f(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}''')
|
||||
self.assertEqual(jsi.call_function('f', '''h 7=g("1j");7.7h({7g:[{33:"w://7f-7e-7d-7c.v.7b/7a/79/78/77/76.74?t=73&s=2s&e=72&f=2t&71=70.0.0.1&6z=6y&6x=6w"}],6v:"w://32.v.u/6u.31",16:"r%",15:"r%",6t:"6s",6r:"",6q:"l",6p:"l",6o:"6n",6m:\'6l\',6k:"6j",9:[{33:"/2u?b=6i&n=50&6h=w://32.v.u/6g.31",6f:"6e"}],1y:{6d:1,6c:\'#6b\',6a:\'#69\',68:"67",66:30,65:r,},"64":{63:"%62 2m%m%61%5z%5y%5x.u%5w%5v%5u.2y%22 2k%m%1o%22 5t%m%1o%22 5s%m%1o%22 2j%m%5r%22 16%m%5q%22 15%m%5p%22 5o%2z%5n%5m%2z",5l:"w://v.u/d/1k/5k.2y",5j:[]},\'5i\':{"5h":"5g"},5f:"5e",5d:"w://v.u",5c:{},5b:l,1x:[0.25,0.50,0.75,1,1.25,1.5,2]});h 1m,1n,5a;h 59=0,58=0;h 7=g("1j");h 2x=0,57=0,56=0;$.55({54:{\'53-52\':\'2i-51\'}});7.j(\'4z\',6(x){c(5>0&&x.1l>=5&&1n!=1){1n=1;$(\'q.4y\').4x(\'4w\')}});7.j(\'13\',6(x){2x=x.1l});7.j(\'2g\',6(x){2w(x)});7.j(\'4v\',6(){$(\'q.2v\').4u()});6 2w(x){$(\'q.2v\').4t();c(1m)19;1m=1;17=0;c(4s.4r===l){17=1}$.4q(\'/2u?b=4p&2l=1k&4o=2t-4n-4m-2s-4l&4k=&4j=&4i=&17=\'+17,6(2r){$(\'#4h\').4g(2r)});$(\'.3-8-4f-4e:4d("4c")\').2h(6(e){2q();g().4b(0);g().4a(l)});6 2q(){h $14=$("<q />").2p({1l:"49",16:"r%",15:"r%",48:0,2n:0,2o:47,46:"45(10%, 10%, 10%, 0.4)","44-43":"42"});$("<41 />").2p({16:"60%",15:"60%",2o:40,"3z-2n":"3y"}).3x({\'2m\':\'/?b=3w&2l=1k\',\'2k\':\'0\',\'2j\':\'2i\'}).2f($14);$14.2h(6(){$(3v).3u();g().2g()});$14.2f($(\'#1j\'))}g().13(0);}6 3t(){h 9=7.1b(2e);2d.2c(9);c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==2e){2d.2c(\'!!=\'+i);7.1p(i)}}}}7.j(\'3s\',6(){g().1h("/2a/3r.29","3q 10 28",6(){g().13(g().27()+10)},"2b");$("q[26=2b]").23().21(\'.3-20-1z\');g().1h("/2a/3p.29","3o 10 28",6(){h 12=g().27()-10;c(12<0)12=0;g().13(12)},"24");$("q[26=24]").23().21(\'.3-20-1z\');});6 1i(){}7.j(\'3n\',6(){1i()});7.j(\'3m\',6(){1i()});7.j("k",6(y){h 9=7.1b();c(9.n<2)19;$(\'.3-8-3l-3k\').3j(6(){$(\'#3-8-a-k\').1e(\'3-8-a-z\');$(\'.3-a-k\').p(\'o-1f\',\'11\')});7.1h("/3i/3h.3g","3f 3e",6(){$(\'.3-1w\').3d(\'3-8-1v\');$(\'.3-8-1y, .3-8-1x\').p(\'o-1g\',\'11\');c($(\'.3-1w\').3c(\'3-8-1v\')){$(\'.3-a-k\').p(\'o-1g\',\'l\');$(\'.3-a-k\').p(\'o-1f\',\'l\');$(\'.3-8-a\').1e(\'3-8-a-z\');$(\'.3-8-a:1u\').3b(\'3-8-a-z\')}3a{$(\'.3-a-k\').p(\'o-1g\',\'11\');$(\'.3-a-k\').p(\'o-1f\',\'11\');$(\'.3-8-a:1u\').1e(\'3-8-a-z\')}},"39");7.j("38",6(y){1d.37(\'1c\',y.9[y.36].1a)});c(1d.1t(\'1c\')){35("1s(1d.1t(\'1c\'));",34)}});h 18;6 1s(1q){h 9=7.1b();c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==1q){c(i==18){19}18=i;7.1p(i)}}}}',36,270,'|||jw|||function|player|settings|tracks|submenu||if||||jwplayer|var||on|audioTracks|true|3D|length|aria|attr|div|100|||sx|filemoon|https||event|active||false|tt|seek|dd|height|width|adb|current_audio|return|name|getAudioTracks|default_audio|localStorage|removeClass|expanded|checked|addButton|callMeMaybe|vplayer|0fxcyc2ajhp1|position|vvplay|vvad|220|setCurrentAudioTrack|audio_name|for|audio_set|getItem|last|open|controls|playbackRates|captions|rewind|icon|insertAfter||detach|ff00||button|getPosition|sec|png|player8|ff11|log|console|track_name|appendTo|play|click|no|scrolling|frameborder|file_code|src|top|zIndex|css|showCCform|data|1662367683|383371|dl|video_ad|doPlay|prevt|mp4|3E||jpg|thumbs|file|300|setTimeout|currentTrack|setItem|audioTrackChanged|dualSound|else|addClass|hasClass|toggleClass|Track|Audio|svg|dualy|images|mousedown|buttons|topbar|playAttemptFailed|beforePlay|Rewind|fr|Forward|ff|ready|set_audio_track|remove|this|upload_srt|prop|50px|margin|1000001|iframe|center|align|text|rgba|background|1000000|left|absolute|pause|setCurrentCaptions|Upload|contains|item|content|html|fviews|referer|prem|embed|3e57249ef633e0d03bf76ceb8d8a4b65|216|83|hash|view|get|TokenZir|window|hide|show|complete|slow|fadeIn|video_ad_fadein|time||cache|Cache|Content|headers|ajaxSetup|v2done|tott|vastdone2|vastdone1|vvbefore|playbackRateControls|cast|aboutlink|FileMoon|abouttext|UHD|1870|qualityLabels|sites|GNOME_POWER|link|2Fiframe|3C|allowfullscreen|22360|22640|22no|marginheight|marginwidth|2FGNOME_POWER|2F0fxcyc2ajhp1|2Fe|2Ffilemoon|2F|3A||22https|3Ciframe|code|sharing|fontOpacity|backgroundOpacity|Tahoma|fontFamily|303030|backgroundColor|FFFFFF|color|userFontScale|thumbnails|kind|0fxcyc2ajhp10000|url|get_slides|start|startparam|none|preload|html5|primary|hlshtml|androidhls|duration|uniform|stretching|0fxcyc2ajhp1_xt|image|2048|sp|6871|asn|127|srv|43200|_g3XlBcu2lmD9oDexD2NLWSmah2Nu3XcDrl93m9PwXY|m3u8||master|0fxcyc2ajhp1_x|00076|01|hls2|to|s01|delivery|storage|moon|sources|setup'''.split('|')))
|
||||
self.assertEqual(jsi.call_function('f', '''h 7=g("1j");7.7h({7g:[{33:"w://7f-7e-7d-7c.v.7b/7a/79/78/77/76.74?t=73&s=2s&e=72&f=2t&71=70.0.0.1&6z=6y&6x=6w"}],6v:"w://32.v.u/6u.31",16:"r%",15:"r%",6t:"6s",6r:"",6q:"l",6p:"l",6o:"6n",6m:\'6l\',6k:"6j",9:[{33:"/2u?b=6i&n=50&6h=w://32.v.u/6g.31",6f:"6e"}],1y:{6d:1,6c:\'#6b\',6a:\'#69\',68:"67",66:30,65:r,},"64":{63:"%62 2m%m%61%5z%5y%5x.u%5w%5v%5u.2y%22 2k%m%1o%22 5t%m%1o%22 5s%m%1o%22 2j%m%5r%22 16%m%5q%22 15%m%5p%22 5o%2z%5n%5m%2z",5l:"w://v.u/d/1k/5k.2y",5j:[]},\'5i\':{"5h":"5g"},5f:"5e",5d:"w://v.u",5c:{},5b:l,1x:[0.25,0.50,0.75,1,1.25,1.5,2]});h 1m,1n,5a;h 59=0,58=0;h 7=g("1j");h 2x=0,57=0,56=0;$.55({54:{\'53-52\':\'2i-51\'}});7.j(\'4z\',6(x){c(5>0&&x.1l>=5&&1n!=1){1n=1;$(\'q.4y\').4x(\'4w\')}});7.j(\'13\',6(x){2x=x.1l});7.j(\'2g\',6(x){2w(x)});7.j(\'4v\',6(){$(\'q.2v\').4u()});6 2w(x){$(\'q.2v\').4t();c(1m)19;1m=1;17=0;c(4s.4r===l){17=1}$.4q(\'/2u?b=4p&2l=1k&4o=2t-4n-4m-2s-4l&4k=&4j=&4i=&17=\'+17,6(2r){$(\'#4h\').4g(2r)});$(\'.3-8-4f-4e:4d("4c")\').2h(6(e){2q();g().4b(0);g().4a(l)});6 2q(){h $14=$("<q />").2p({1l:"49",16:"r%",15:"r%",48:0,2n:0,2o:47,46:"45(10%, 10%, 10%, 0.4)","44-43":"42"});$("<41 />").2p({16:"60%",15:"60%",2o:40,"3z-2n":"3y"}).3x({\'2m\':\'/?b=3w&2l=1k\',\'2k\':\'0\',\'2j\':\'2i\'}).2f($14);$14.2h(6(){$(3v).3u();g().2g()});$14.2f($(\'#1j\'))}g().13(0);}6 3t(){h 9=7.1b(2e);2d.2c(9);c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==2e){2d.2c(\'!!=\'+i);7.1p(i)}}}}7.j(\'3s\',6(){g().1h("/2a/3r.29","3q 10 28",6(){g().13(g().27()+10)},"2b");$("q[26=2b]").23().21(\'.3-20-1z\');g().1h("/2a/3p.29","3o 10 28",6(){h 12=g().27()-10;c(12<0)12=0;g().13(12)},"24");$("q[26=24]").23().21(\'.3-20-1z\');});6 1i(){}7.j(\'3n\',6(){1i()});7.j(\'3m\',6(){1i()});7.j("k",6(y){h 9=7.1b();c(9.n<2)19;$(\'.3-8-3l-3k\').3j(6(){$(\'#3-8-a-k\').1e(\'3-8-a-z\');$(\'.3-a-k\').p(\'o-1f\',\'11\')});7.1h("/3i/3h.3g","3f 3e",6(){$(\'.3-1w\').3d(\'3-8-1v\');$(\'.3-8-1y, .3-8-1x\').p(\'o-1g\',\'11\');c($(\'.3-1w\').3c(\'3-8-1v\')){$(\'.3-a-k\').p(\'o-1g\',\'l\');$(\'.3-a-k\').p(\'o-1f\',\'l\');$(\'.3-8-a\').1e(\'3-8-a-z\');$(\'.3-8-a:1u\').3b(\'3-8-a-z\')}3a{$(\'.3-a-k\').p(\'o-1g\',\'11\');$(\'.3-a-k\').p(\'o-1f\',\'11\');$(\'.3-8-a:1u\').1e(\'3-8-a-z\')}},"39");7.j("38",6(y){1d.37(\'1c\',y.9[y.36].1a)});c(1d.1t(\'1c\')){35("1s(1d.1t(\'1c\'));",34)}});h 18;6 1s(1q){h 9=7.1b();c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==1q){c(i==18){19}18=i;7.1p(i)}}}}',36,270,'|||jw|||function|player|settings|tracks|submenu||if||||jwplayer|var||on|audioTracks|true|3D|length|aria|attr|div|100|||sx|filemoon|https||event|active||false|tt|seek|dd|height|width|adb|current_audio|return|name|getAudioTracks|default_audio|localStorage|removeClass|expanded|checked|addButton|callMeMaybe|vplayer|0fxcyc2ajhp1|position|vvplay|vvad|220|setCurrentAudioTrack|audio_name|for|audio_set|getItem|last|open|controls|playbackRates|captions|rewind|icon|insertAfter||detach|ff00||button|getPosition|sec|png|player8|ff11|log|console|track_name|appendTo|play|click|no|scrolling|frameborder|file_code|src|top|zIndex|css|showCCform|data|1662367683|383371|dl|video_ad|doPlay|prevt|mp4|3E||jpg|thumbs|file|300|setTimeout|currentTrack|setItem|audioTrackChanged|dualSound|else|addClass|hasClass|toggleClass|Track|Audio|svg|dualy|images|mousedown|buttons|topbar|playAttemptFailed|beforePlay|Rewind|fr|Forward|ff|ready|set_audio_track|remove|this|upload_srt|prop|50px|margin|1000001|iframe|center|align|text|rgba|background|1000000|left|absolute|pause|setCurrentCaptions|Upload|contains|item|content|html|fviews|referer|prem|embed|3e57249ef633e0d03bf76ceb8d8a4b65|216|83|hash|view|get|TokenZir|window|hide|show|complete|slow|fadeIn|video_ad_fadein|time||cache|Cache|Content|headers|ajaxSetup|v2done|tott|vastdone2|vastdone1|vvbefore|playbackRateControls|cast|aboutlink|FileMoon|abouttext|UHD|1870|qualityLabels|sites|GNOME_POWER|link|2Fiframe|3C|allowfullscreen|22360|22640|22no|marginheight|marginwidth|2FGNOME_POWER|2F0fxcyc2ajhp1|2Fe|2Ffilemoon|2F|3A||22https|3Ciframe|code|sharing|fontOpacity|backgroundOpacity|Tahoma|fontFamily|303030|backgroundColor|FFFFFF|color|userFontScale|thumbnails|kind|0fxcyc2ajhp10000|url|get_slides|start|startparam|none|preload|html5|primary|hlshtml|androidhls|duration|uniform|stretching|0fxcyc2ajhp1_xt|image|2048|sp|6871|asn|127|srv|43200|_g3XlBcu2lmD9oDexD2NLWSmah2Nu3XcDrl93m9PwXY|m3u8||master|0fxcyc2ajhp1_x|00076|01|hls2|to|s01|delivery|storage|moon|sources|setup'''.split('|'))) # noqa: SIM905
|
||||
|
||||
def test_join(self):
|
||||
test_input = list('test')
|
||||
@@ -462,6 +462,16 @@ class TestJSInterpreter(unittest.TestCase):
|
||||
]:
|
||||
assert js_number_to_string(test, radix) == expected
|
||||
|
||||
def test_extract_function(self):
|
||||
jsi = JSInterpreter('function a(b) { return b + 1; }')
|
||||
func = jsi.extract_function('a')
|
||||
self.assertEqual(func([2]), 3)
|
||||
|
||||
def test_extract_function_with_global_stack(self):
|
||||
jsi = JSInterpreter('function c(d) { return d + e + f + g; }')
|
||||
func = jsi.extract_function('c', {'e': 10}, {'f': 100, 'g': 1000})
|
||||
self.assertEqual(func([1]), 1111)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@@ -219,11 +219,8 @@ class TestUtil(unittest.TestCase):
|
||||
self.assertEqual(sanitize_filename('_BD_eEpuzXw', is_id=True), '_BD_eEpuzXw')
|
||||
self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI')
|
||||
|
||||
@unittest.mock.patch('sys.platform', 'win32')
|
||||
def test_sanitize_path(self):
|
||||
with unittest.mock.patch('sys.platform', 'win32'):
|
||||
self._test_sanitize_path()
|
||||
|
||||
def _test_sanitize_path(self):
|
||||
self.assertEqual(sanitize_path('abc'), 'abc')
|
||||
self.assertEqual(sanitize_path('abc/def'), 'abc\\def')
|
||||
self.assertEqual(sanitize_path('abc\\def'), 'abc\\def')
|
||||
@@ -254,10 +251,8 @@ class TestUtil(unittest.TestCase):
|
||||
|
||||
# Check with nt._path_normpath if available
|
||||
try:
|
||||
import nt
|
||||
|
||||
nt_path_normpath = getattr(nt, '_path_normpath', None)
|
||||
except Exception:
|
||||
from nt import _path_normpath as nt_path_normpath
|
||||
except ImportError:
|
||||
nt_path_normpath = None
|
||||
|
||||
for test, expected in [
|
||||
|
@@ -78,6 +78,11 @@ _SIG_TESTS = [
|
||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
||||
'0QJ8wRAIgXmPlOPSBkkUs1bYFYlJCfe29xxAj7v1pDL0QwbdV96sCIEzpWqMGkFR20CFOg51Tp-7vj_EMu-m37KtXJ2OySqa0q',
|
||||
),
|
||||
(
|
||||
'https://www.youtube.com/s/player/643afba4/tv-player-ias.vflset/tv-player-ias.js',
|
||||
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
|
||||
'AAOAOq0QJ8wRAIgXmPlOPSBkkUs1bYFYlJCfe29xx8j7vgpDL0QwbdV06sCIEzpWqMGkFR20CFOS21Tp-7vj_EMu-m37KtXJoOy1',
|
||||
),
|
||||
]
|
||||
|
||||
_NSIG_TESTS = [
|
||||
@@ -205,6 +210,30 @@ _NSIG_TESTS = [
|
||||
'https://www.youtube.com/s/player/9c6dfc4a/player_ias.vflset/en_US/base.js',
|
||||
'jbu7ylIosQHyJyJV', 'uwI0ESiynAmhNg',
|
||||
),
|
||||
(
|
||||
'https://www.youtube.com/s/player/e7567ecf/player_ias_tce.vflset/en_US/base.js',
|
||||
'Sy4aDGc0VpYRR9ew_', '5UPOT1VhoZxNLQ',
|
||||
),
|
||||
(
|
||||
'https://www.youtube.com/s/player/d50f54ef/player_ias_tce.vflset/en_US/base.js',
|
||||
'Ha7507LzRmH3Utygtj', 'XFTb2HoeOE5MHg',
|
||||
),
|
||||
(
|
||||
'https://www.youtube.com/s/player/074a8365/player_ias_tce.vflset/en_US/base.js',
|
||||
'Ha7507LzRmH3Utygtj', 'ufTsrE0IVYrkl8v',
|
||||
),
|
||||
(
|
||||
'https://www.youtube.com/s/player/643afba4/player_ias.vflset/en_US/base.js',
|
||||
'N5uAlLqm0eg1GyHO', 'dCBQOejdq5s-ww',
|
||||
),
|
||||
(
|
||||
'https://www.youtube.com/s/player/69f581a5/tv-player-ias.vflset/tv-player-ias.js',
|
||||
'-qIP447rVlTTwaZjY', 'KNcGOksBAvwqQg',
|
||||
),
|
||||
(
|
||||
'https://www.youtube.com/s/player/643afba4/tv-player-ias.vflset/tv-player-ias.js',
|
||||
'ir9-V6cdbCiyKxhr', '2PL7ZDYAALMfmA',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -218,6 +247,8 @@ class TestPlayerInfo(unittest.TestCase):
|
||||
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-en_US.vflset/base.js', '64dddad9'),
|
||||
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-phone-de_DE.vflset/base.js', '64dddad9'),
|
||||
('https://www.youtube.com/s/player/64dddad9/player-plasma-ias-tablet-en_US.vflset/base.js', '64dddad9'),
|
||||
('https://www.youtube.com/s/player/e7567ecf/player_ias_tce.vflset/en_US/base.js', 'e7567ecf'),
|
||||
('https://www.youtube.com/s/player/643afba4/tv-player-ias.vflset/tv-player-ias.js', '643afba4'),
|
||||
# obsolete
|
||||
('https://www.youtube.com/yts/jsbin/player_ias-vfle4-e03/en_US/base.js', 'vfle4-e03'),
|
||||
('https://www.youtube.com/yts/jsbin/player_ias-vfl49f_g4/en_US/base.js', 'vfl49f_g4'),
|
||||
@@ -250,7 +281,7 @@ def t_factory(name, sig_func, url_pattern):
|
||||
def make_tfunc(url, sig_input, expected_sig):
|
||||
m = url_pattern.match(url)
|
||||
assert m, f'{url!r} should follow URL format'
|
||||
test_id = m.group('id')
|
||||
test_id = re.sub(r'[/.-]', '_', m.group('id') or m.group('compat_id'))
|
||||
|
||||
def test_func(self):
|
||||
basename = f'player-{name}-{test_id}.js'
|
||||
@@ -279,17 +310,22 @@ def n_sig(jscode, sig_input):
|
||||
ie = YoutubeIE(FakeYDL())
|
||||
funcname = ie._extract_n_function_name(jscode)
|
||||
jsi = JSInterpreter(jscode)
|
||||
func = jsi.extract_function_from_code(*ie._fixup_n_function_code(*jsi.extract_function_code(funcname)))
|
||||
func = jsi.extract_function_from_code(*ie._fixup_n_function_code(*jsi.extract_function_code(funcname), jscode))
|
||||
return func([sig_input])
|
||||
|
||||
|
||||
make_sig_test = t_factory(
|
||||
'signature', signature, re.compile(r'.*(?:-|/player/)(?P<id>[a-zA-Z0-9_-]+)(?:/.+\.js|(?:/watch_as3|/html5player)?\.[a-z]+)$'))
|
||||
'signature', signature,
|
||||
re.compile(r'''(?x)
|
||||
.+(?:
|
||||
/player/(?P<id>[a-zA-Z0-9_/.-]+)|
|
||||
/html5player-(?:en_US-)?(?P<compat_id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player)?
|
||||
)\.js$'''))
|
||||
for test_spec in _SIG_TESTS:
|
||||
make_sig_test(*test_spec)
|
||||
|
||||
make_nsig_test = t_factory(
|
||||
'nsig', n_sig, re.compile(r'.+/player/(?P<id>[a-zA-Z0-9_-]+)/.+.js$'))
|
||||
'nsig', n_sig, re.compile(r'.+/player/(?P<id>[a-zA-Z0-9_/.-]+)\.js$'))
|
||||
for test_spec in _NSIG_TESTS:
|
||||
make_nsig_test(*test_spec)
|
||||
|
||||
|
@@ -83,7 +83,7 @@ def aes_ecb_encrypt(data, key, iv=None):
|
||||
@returns {int[]} encrypted data
|
||||
"""
|
||||
expanded_key = key_expansion(key)
|
||||
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||
block_count = ceil(len(data) / BLOCK_SIZE_BYTES)
|
||||
|
||||
encrypted_data = []
|
||||
for i in range(block_count):
|
||||
@@ -103,7 +103,7 @@ def aes_ecb_decrypt(data, key, iv=None):
|
||||
@returns {int[]} decrypted data
|
||||
"""
|
||||
expanded_key = key_expansion(key)
|
||||
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||
block_count = ceil(len(data) / BLOCK_SIZE_BYTES)
|
||||
|
||||
encrypted_data = []
|
||||
for i in range(block_count):
|
||||
@@ -134,7 +134,7 @@ def aes_ctr_encrypt(data, key, iv):
|
||||
@returns {int[]} encrypted data
|
||||
"""
|
||||
expanded_key = key_expansion(key)
|
||||
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||
block_count = ceil(len(data) / BLOCK_SIZE_BYTES)
|
||||
counter = iter_vector(iv)
|
||||
|
||||
encrypted_data = []
|
||||
@@ -158,7 +158,7 @@ def aes_cbc_decrypt(data, key, iv):
|
||||
@returns {int[]} decrypted data
|
||||
"""
|
||||
expanded_key = key_expansion(key)
|
||||
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||
block_count = ceil(len(data) / BLOCK_SIZE_BYTES)
|
||||
|
||||
decrypted_data = []
|
||||
previous_cipher_block = iv
|
||||
@@ -183,7 +183,7 @@ def aes_cbc_encrypt(data, key, iv, *, padding_mode='pkcs7'):
|
||||
@returns {int[]} encrypted data
|
||||
"""
|
||||
expanded_key = key_expansion(key)
|
||||
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||
block_count = ceil(len(data) / BLOCK_SIZE_BYTES)
|
||||
|
||||
encrypted_data = []
|
||||
previous_cipher_block = iv
|
||||
|
@@ -1055,6 +1055,7 @@ from .livestream import (
|
||||
)
|
||||
from .livestreamfails import LivestreamfailsIE
|
||||
from .lnk import LnkIE
|
||||
from .loco import LocoIE
|
||||
from .loom import (
|
||||
LoomFolderIE,
|
||||
LoomIE,
|
||||
@@ -2402,7 +2403,6 @@ from .voxmedia import (
|
||||
from .vrt import (
|
||||
VRTIE,
|
||||
DagelijkseKostIE,
|
||||
KetnetIE,
|
||||
Radio1BeIE,
|
||||
VrtNUIE,
|
||||
)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import datetime as dt
|
||||
import functools
|
||||
|
||||
from .common import InfoExtractor
|
||||
@@ -10,7 +11,7 @@ from ..utils import (
|
||||
filter_dict,
|
||||
int_or_none,
|
||||
orderedSet,
|
||||
unified_timestamp,
|
||||
parse_iso8601,
|
||||
url_or_none,
|
||||
urlencode_postdata,
|
||||
urljoin,
|
||||
@@ -87,9 +88,9 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
||||
'uploader_id': 'rlantnghks',
|
||||
'uploader': '페이즈으',
|
||||
'duration': 10840,
|
||||
'thumbnail': r're:https?://videoimg\.sooplive\.co/.kr/.+',
|
||||
'thumbnail': r're:https?://videoimg\.(?:sooplive\.co\.kr|afreecatv\.com)/.+',
|
||||
'upload_date': '20230108',
|
||||
'timestamp': 1673218805,
|
||||
'timestamp': 1673186405,
|
||||
'title': '젠지 페이즈',
|
||||
},
|
||||
'params': {
|
||||
@@ -102,7 +103,7 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
||||
'id': '20170411_BE689A0E_190960999_1_2_h',
|
||||
'ext': 'mp4',
|
||||
'title': '혼자사는여자집',
|
||||
'thumbnail': r're:https?://(?:video|st)img\.sooplive\.co\.kr/.+',
|
||||
'thumbnail': r're:https?://(?:video|st)img\.(?:sooplive\.co\.kr|afreecatv\.com)/.+',
|
||||
'uploader': '♥이슬이',
|
||||
'uploader_id': 'dasl8121',
|
||||
'upload_date': '20170411',
|
||||
@@ -119,7 +120,7 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
||||
'id': '20180327_27901457_202289533_1',
|
||||
'ext': 'mp4',
|
||||
'title': '[생]빨개요♥ (part 1)',
|
||||
'thumbnail': r're:https?://(?:video|st)img\.sooplive\.co\.kr/.+',
|
||||
'thumbnail': r're:https?://(?:video|st)img\.(?:sooplive\.co\.kr|afreecatv\.com)/.+',
|
||||
'uploader': '[SA]서아',
|
||||
'uploader_id': 'bjdyrksu',
|
||||
'upload_date': '20180327',
|
||||
@@ -187,7 +188,7 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
||||
'formats': formats,
|
||||
**traverse_obj(file_element, {
|
||||
'duration': ('duration', {int_or_none(scale=1000)}),
|
||||
'timestamp': ('file_start', {unified_timestamp}),
|
||||
'timestamp': ('file_start', {parse_iso8601(delimiter=' ', timezone=dt.timedelta(hours=9))}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -370,7 +371,7 @@ class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
||||
'title': channel_info.get('TITLE') or station_info.get('station_title'),
|
||||
'uploader': channel_info.get('BJNICK') or station_info.get('station_name'),
|
||||
'uploader_id': broadcaster_id,
|
||||
'timestamp': unified_timestamp(station_info.get('broad_start')),
|
||||
'timestamp': parse_iso8601(station_info.get('broad_start'), delimiter=' ', timezone=dt.timedelta(hours=9)),
|
||||
'formats': formats,
|
||||
'is_live': True,
|
||||
'http_headers': {'Referer': url},
|
||||
|
@@ -86,7 +86,7 @@ class BandlabBaseIE(InfoExtractor):
|
||||
'webpage_url': (
|
||||
'id', ({value(url)}, {format_field(template='https://www.bandlab.com/post/%s')}), filter, any),
|
||||
'url': ('video', 'url', {url_or_none}),
|
||||
'title': ('caption', {lambda x: x.replace('\n', ' ')}, {truncate_string(left=50)}),
|
||||
'title': ('caption', {lambda x: x.replace('\n', ' ')}, {truncate_string(left=72)}),
|
||||
'description': ('caption', {str}),
|
||||
'thumbnail': ('video', 'picture', 'url', {url_or_none}),
|
||||
'view_count': ('video', 'counters', 'plays', {int_or_none}),
|
||||
@@ -120,7 +120,7 @@ class BandlabIE(BandlabBaseIE):
|
||||
'duration': 54.629999999999995,
|
||||
'title': 'sweet black',
|
||||
'upload_date': '20231210',
|
||||
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/songs/fa082beb-b856-4730-9170-a57e4e32cc2c/',
|
||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/songs/fa082beb-b856-4730-9170-a57e4e32cc2c/',
|
||||
'genres': ['Lofi'],
|
||||
'uploader': 'ender milze',
|
||||
'comment_count': int,
|
||||
@@ -142,7 +142,7 @@ class BandlabIE(BandlabBaseIE):
|
||||
'duration': 54.629999999999995,
|
||||
'title': 'sweet black',
|
||||
'upload_date': '20231210',
|
||||
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/songs/fa082beb-b856-4730-9170-a57e4e32cc2c/',
|
||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/songs/fa082beb-b856-4730-9170-a57e4e32cc2c/',
|
||||
'genres': ['Lofi'],
|
||||
'uploader': 'ender milze',
|
||||
'comment_count': int,
|
||||
@@ -158,7 +158,7 @@ class BandlabIE(BandlabBaseIE):
|
||||
'comment_count': int,
|
||||
'genres': ['Other'],
|
||||
'uploader_id': 'user8353034818103753',
|
||||
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/songs/51b18363-da23-4b9b-a29c-2933a3e561ca/',
|
||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/songs/51b18363-da23-4b9b-a29c-2933a3e561ca/',
|
||||
'timestamp': 1709625771,
|
||||
'track': 'PodcastMaerchen4b',
|
||||
'duration': 468.14,
|
||||
@@ -178,7 +178,7 @@ class BandlabIE(BandlabBaseIE):
|
||||
'id': '110343fc-148b-ea11-96d2-0003ffd1fc09',
|
||||
'ext': 'm4a',
|
||||
'timestamp': 1588273294,
|
||||
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/users/b612e533-e4f7-4542-9f50-3fcfd8dd822c/',
|
||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/users/b612e533-e4f7-4542-9f50-3fcfd8dd822c/',
|
||||
'description': 'Final Revision.',
|
||||
'title': 'Replay ( Instrumental)',
|
||||
'uploader': 'David R Sparks',
|
||||
@@ -200,7 +200,7 @@ class BandlabIE(BandlabBaseIE):
|
||||
'id': '5cdf9036-3857-ef11-991a-6045bd36e0d9',
|
||||
'ext': 'mp4',
|
||||
'duration': 44.705,
|
||||
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/videos/67c6cef1-cef6-40d3-831e-a55bc1dcb972/',
|
||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/videos/67c6cef1-cef6-40d3-831e-a55bc1dcb972/',
|
||||
'comment_count': int,
|
||||
'title': 'backing vocals',
|
||||
'uploader_id': 'marliashya',
|
||||
@@ -224,7 +224,7 @@ class BandlabIE(BandlabBaseIE):
|
||||
'view_count': int,
|
||||
'track': 'Positronic Meltdown',
|
||||
'duration': 318.55,
|
||||
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/songs/87165bc3-5439-496e-b1f7-a9f13b541ff2/',
|
||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/songs/87165bc3-5439-496e-b1f7-a9f13b541ff2/',
|
||||
'description': 'Checkout my tracks at AOMX http://aomxsounds.com/',
|
||||
'uploader_id': 'microfreaks',
|
||||
'title': 'Positronic Meltdown',
|
||||
@@ -246,7 +246,7 @@ class BandlabIE(BandlabBaseIE):
|
||||
'comment_count': int,
|
||||
'uploader': 'Sorakime',
|
||||
'uploader_id': 'sorakime',
|
||||
'thumbnail': 'https://bandlabimages.azureedge.net/v1.0/users/572a351a-0f3a-4c6a-ac39-1a5defdeeb1c/',
|
||||
'thumbnail': 'https://bl-prod-images.azureedge.net/v1.0/users/572a351a-0f3a-4c6a-ac39-1a5defdeeb1c/',
|
||||
'timestamp': 1691162128,
|
||||
'upload_date': '20230804',
|
||||
'media_type': 'track',
|
||||
|
@@ -1596,16 +1596,16 @@ class BilibiliPlaylistIE(BilibiliSpaceListBaseIE):
|
||||
|
||||
webpage = self._download_webpage(url, list_id)
|
||||
initial_state = self._search_json(r'window\.__INITIAL_STATE__\s*=', webpage, 'initial state', list_id)
|
||||
if traverse_obj(initial_state, ('error', 'code', {int_or_none})) != 200:
|
||||
error_code = traverse_obj(initial_state, ('error', 'trueCode', {int_or_none}))
|
||||
error_message = traverse_obj(initial_state, ('error', 'message', {str_or_none}))
|
||||
error = traverse_obj(initial_state, (('error', 'listError'), all, lambda _, v: v['code'], any))
|
||||
if error and error['code'] != 200:
|
||||
error_code = error.get('trueCode')
|
||||
if error_code == -400 and list_id == 'watchlater':
|
||||
self.raise_login_required('You need to login to access your watchlater playlist')
|
||||
elif error_code == -403:
|
||||
self.raise_login_required('This is a private playlist. You need to login as its owner')
|
||||
elif error_code == 11010:
|
||||
raise ExtractorError('Playlist is no longer available', expected=True)
|
||||
raise ExtractorError(f'Could not access playlist: {error_code} {error_message}')
|
||||
raise ExtractorError(f'Could not access playlist: {error_code} {error.get("message")}')
|
||||
|
||||
query = {
|
||||
'ps': 20,
|
||||
|
@@ -53,7 +53,7 @@ class BlueskyIE(InfoExtractor):
|
||||
'channel_id': 'did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||
'channel_url': 'https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||
'title': 'Bluesky now has video! Update your app to versi...',
|
||||
'title': 'Bluesky now has video! Update your app to version 1.91 or refresh on ...',
|
||||
'alt_title': 'Bluesky video feature announcement',
|
||||
'description': r're:(?s)Bluesky now has video! .{239}',
|
||||
'upload_date': '20240911',
|
||||
@@ -172,7 +172,7 @@ class BlueskyIE(InfoExtractor):
|
||||
'channel_id': 'did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||
'channel_url': 'https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||
'title': 'Bluesky now has video! Update your app to versi...',
|
||||
'title': 'Bluesky now has video! Update your app to version 1.91 or refresh on ...',
|
||||
'alt_title': 'Bluesky video feature announcement',
|
||||
'description': r're:(?s)Bluesky now has video! .{239}',
|
||||
'upload_date': '20240911',
|
||||
@@ -191,7 +191,7 @@ class BlueskyIE(InfoExtractor):
|
||||
'info_dict': {
|
||||
'id': '3l7rdfxhyds2f',
|
||||
'ext': 'mp4',
|
||||
'uploader': 'cinnamon',
|
||||
'uploader': 'cinnamon 🐇 🏳️⚧️',
|
||||
'uploader_id': 'cinny.bun.how',
|
||||
'uploader_url': 'https://bsky.app/profile/cinny.bun.how',
|
||||
'channel_id': 'did:plc:7x6rtuenkuvxq3zsvffp2ide',
|
||||
@@ -255,7 +255,7 @@ class BlueskyIE(InfoExtractor):
|
||||
'info_dict': {
|
||||
'id': '3l77u64l7le2e',
|
||||
'ext': 'mp4',
|
||||
'title': 'hearing people on twitter say that bluesky isn\'...',
|
||||
'title': "hearing people on twitter say that bluesky isn't funny yet so post t...",
|
||||
'like_count': int,
|
||||
'uploader_id': 'thafnine.net',
|
||||
'uploader_url': 'https://bsky.app/profile/thafnine.net',
|
||||
@@ -387,7 +387,7 @@ class BlueskyIE(InfoExtractor):
|
||||
'age_limit': (
|
||||
'labels', ..., 'val', {lambda x: 18 if x in ('sexual', 'porn', 'graphic-media') else None}, any),
|
||||
'description': (*record_path, 'text', {str}, filter),
|
||||
'title': (*record_path, 'text', {lambda x: x.replace('\n', ' ')}, {truncate_string(left=50)}),
|
||||
'title': (*record_path, 'text', {lambda x: x.replace('\n', ' ')}, {truncate_string(left=72)}),
|
||||
}),
|
||||
})
|
||||
return entries
|
||||
|
@@ -21,7 +21,7 @@ class CHZZKLiveIE(InfoExtractor):
|
||||
'channel': '진짜도현',
|
||||
'channel_id': 'c68b8ef525fb3d2fa146344d84991753',
|
||||
'channel_is_verified': False,
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||
'timestamp': 1705510344,
|
||||
'upload_date': '20240117',
|
||||
'live_status': 'is_live',
|
||||
@@ -98,7 +98,7 @@ class CHZZKVideoIE(InfoExtractor):
|
||||
'channel': '침착맨',
|
||||
'channel_id': 'bb382c2c0cc9fa7c86ab3b037fb5799c',
|
||||
'channel_is_verified': False,
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||
'duration': 15577,
|
||||
'timestamp': 1702970505.417,
|
||||
'upload_date': '20231219',
|
||||
@@ -115,7 +115,7 @@ class CHZZKVideoIE(InfoExtractor):
|
||||
'channel': '라디유radiyu',
|
||||
'channel_id': '68f895c59a1043bc5019b5e08c83a5c5',
|
||||
'channel_is_verified': False,
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||
'duration': 95,
|
||||
'timestamp': 1703102631.722,
|
||||
'upload_date': '20231220',
|
||||
@@ -131,12 +131,30 @@ class CHZZKVideoIE(InfoExtractor):
|
||||
'channel': '강지',
|
||||
'channel_id': 'b5ed5db484d04faf4d150aedd362f34b',
|
||||
'channel_is_verified': True,
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||
'duration': 4433,
|
||||
'timestamp': 1703307460.214,
|
||||
'upload_date': '20231223',
|
||||
'view_count': int,
|
||||
},
|
||||
}, {
|
||||
# video_status == 'NONE' but is downloadable
|
||||
'url': 'https://chzzk.naver.com/video/6325166',
|
||||
'info_dict': {
|
||||
'id': '6325166',
|
||||
'ext': 'mp4',
|
||||
'title': '와이프 숙제빼주기',
|
||||
'channel': '이 다',
|
||||
'channel_id': '0076a519f147ee9fd0959bf02f9571ca',
|
||||
'channel_is_verified': False,
|
||||
'view_count': int,
|
||||
'duration': 28167,
|
||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||
'timestamp': 1742139216.86,
|
||||
'upload_date': '20250316',
|
||||
'live_status': 'was_live',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
@@ -147,11 +165,7 @@ class CHZZKVideoIE(InfoExtractor):
|
||||
|
||||
live_status = 'was_live' if video_meta.get('liveOpenDate') else 'not_live'
|
||||
video_status = video_meta.get('vodStatus')
|
||||
if video_status == 'UPLOAD':
|
||||
playback = self._parse_json(video_meta['liveRewindPlaybackJson'], video_id)
|
||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||
playback['media'][0]['path'], video_id, 'mp4', m3u8_id='hls')
|
||||
elif video_status == 'ABR_HLS':
|
||||
if video_status == 'ABR_HLS':
|
||||
formats, subtitles = self._extract_mpd_formats_and_subtitles(
|
||||
f'https://apis.naver.com/neonplayer/vodplay/v1/playback/{video_meta["videoId"]}',
|
||||
video_id, query={
|
||||
@@ -161,10 +175,17 @@ class CHZZKVideoIE(InfoExtractor):
|
||||
'cpl': 'en_US',
|
||||
})
|
||||
else:
|
||||
self.raise_no_formats(
|
||||
f'Unknown video status detected: "{video_status}"', expected=True, video_id=video_id)
|
||||
formats, subtitles = [], {}
|
||||
live_status = 'post_live' if live_status == 'was_live' else None
|
||||
fatal = video_status == 'UPLOAD'
|
||||
playback = self._parse_json(video_meta['liveRewindPlaybackJson'], video_id, fatal=fatal)
|
||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||
traverse_obj(playback, ('media', 0, 'path')), video_id, 'mp4', m3u8_id='hls', fatal=fatal)
|
||||
if formats and video_status != 'UPLOAD':
|
||||
self.write_debug(f'Video found with status: "{video_status}"')
|
||||
elif not formats:
|
||||
self.raise_no_formats(
|
||||
f'Unknown video status detected: "{video_status}"', expected=True, video_id=video_id)
|
||||
formats, subtitles = [], {}
|
||||
live_status = 'post_live' if live_status == 'was_live' else None
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
|
@@ -2935,8 +2935,7 @@ class InfoExtractor:
|
||||
segment_duration = None
|
||||
if 'total_number' not in representation_ms_info and 'segment_duration' in representation_ms_info:
|
||||
segment_duration = float_or_none(representation_ms_info['segment_duration'], representation_ms_info['timescale'])
|
||||
representation_ms_info['total_number'] = int(math.ceil(
|
||||
float_or_none(period_duration, segment_duration, default=0)))
|
||||
representation_ms_info['total_number'] = math.ceil(float_or_none(period_duration, segment_duration, default=0))
|
||||
representation_ms_info['fragments'] = [{
|
||||
media_location_key: media_template % {
|
||||
'Number': segment_number,
|
||||
|
@@ -2,10 +2,12 @@ import hashlib
|
||||
import random
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..networking import HEADRequest
|
||||
from ..utils import (
|
||||
clean_html,
|
||||
int_or_none,
|
||||
try_get,
|
||||
urlhandle_detect_ext,
|
||||
)
|
||||
|
||||
|
||||
@@ -27,7 +29,7 @@ class JamendoIE(InfoExtractor):
|
||||
'ext': 'flac',
|
||||
# 'title': 'Maya Filipič - Stories from Emona I',
|
||||
'title': 'Stories from Emona I',
|
||||
'artist': 'Maya Filipič',
|
||||
'artists': ['Maya Filipič'],
|
||||
'album': 'Between two worlds',
|
||||
'track': 'Stories from Emona I',
|
||||
'duration': 210,
|
||||
@@ -93,9 +95,15 @@ class JamendoIE(InfoExtractor):
|
||||
if not cover_url or cover_url in urls:
|
||||
continue
|
||||
urls.append(cover_url)
|
||||
urlh = self._request_webpage(
|
||||
HEADRequest(cover_url), track_id, 'Checking thumbnail extension',
|
||||
errnote=False, fatal=False)
|
||||
if not urlh:
|
||||
continue
|
||||
size = int_or_none(cover_id.lstrip('size'))
|
||||
thumbnails.append({
|
||||
'id': cover_id,
|
||||
'ext': urlhandle_detect_ext(urlh, default='jpg'),
|
||||
'url': cover_url,
|
||||
'width': size,
|
||||
'height': size,
|
||||
|
87
yt-dlp/yt_dlp/extractor/loco.py
Normal file
87
yt-dlp/yt_dlp/extractor/loco.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from .common import InfoExtractor
|
||||
from ..utils import int_or_none, url_or_none
|
||||
from ..utils.traversal import require, traverse_obj
|
||||
|
||||
|
||||
class LocoIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?loco\.com/(?P<type>streamers|stream)/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://loco.com/streamers/teuzinfps',
|
||||
'info_dict': {
|
||||
'id': 'teuzinfps',
|
||||
'ext': 'mp4',
|
||||
'title': r're:MS BOLADAO, RESENHA & GAMEPLAY ALTO NIVEL',
|
||||
'description': 'bom e novo',
|
||||
'uploader_id': 'RLUVE3S9JU',
|
||||
'channel': 'teuzinfps',
|
||||
'channel_follower_count': int,
|
||||
'comment_count': int,
|
||||
'view_count': int,
|
||||
'concurrent_view_count': int,
|
||||
'like_count': int,
|
||||
'thumbnail': 'https://static.ivory.getloconow.com/default_thumb/743701a9-98ca-41ae-9a8b-70bd5da070ad.jpg',
|
||||
'tags': ['MMORPG', 'Gameplay'],
|
||||
'series': 'Tibia',
|
||||
'timestamp': int,
|
||||
'modified_timestamp': int,
|
||||
'live_status': 'is_live',
|
||||
'upload_date': str,
|
||||
'modified_date': str,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': 'Livestream',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://loco.com/stream/c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
|
||||
'md5': '45ebc8a47ee1c2240178757caf8881b5',
|
||||
'info_dict': {
|
||||
'id': 'c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
|
||||
'ext': 'mp4',
|
||||
'title': 'PAULINHO LOKO NA LOCO!',
|
||||
'description': 'live on na loco',
|
||||
'uploader_id': '2MDO7Z1DPM',
|
||||
'channel': 'paulinholokobr',
|
||||
'channel_follower_count': int,
|
||||
'comment_count': int,
|
||||
'view_count': int,
|
||||
'concurrent_view_count': int,
|
||||
'like_count': int,
|
||||
'duration': 14491,
|
||||
'thumbnail': 'https://static.ivory.getloconow.com/default_thumb/59b5970b-23c1-4518-9e96-17ce341299fe.jpg',
|
||||
'tags': ['Gameplay'],
|
||||
'series': 'GTA 5',
|
||||
'timestamp': 1740612872,
|
||||
'modified_timestamp': 1740613037,
|
||||
'upload_date': '20250226',
|
||||
'modified_date': '20250226',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_type, video_id = self._match_valid_url(url).group('type', 'id')
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
stream = traverse_obj(self._search_nextjs_data(webpage, video_id), (
|
||||
'props', 'pageProps', ('liveStreamData', 'stream'), {dict}, any, {require('stream info')}))
|
||||
|
||||
return {
|
||||
'formats': self._extract_m3u8_formats(stream['conf']['hls'], video_id),
|
||||
'id': video_id,
|
||||
'is_live': video_type == 'streamers',
|
||||
**traverse_obj(stream, {
|
||||
'title': ('title', {str}),
|
||||
'series': ('game_name', {str}),
|
||||
'uploader_id': ('user_uid', {str}),
|
||||
'channel': ('alias', {str}),
|
||||
'description': ('description', {str}),
|
||||
'concurrent_view_count': ('viewersCurrent', {int_or_none}),
|
||||
'view_count': ('total_views', {int_or_none}),
|
||||
'thumbnail': ('thumbnail_url_small', {url_or_none}),
|
||||
'like_count': ('likes', {int_or_none}),
|
||||
'tags': ('tags', ..., {str}),
|
||||
'timestamp': ('started_at', {int_or_none(scale=1000)}),
|
||||
'modified_timestamp': ('updated_at', {int_or_none(scale=1000)}),
|
||||
'comment_count': ('comments_count', {int_or_none}),
|
||||
'channel_follower_count': ('followers_count', {int_or_none}),
|
||||
'duration': ('duration', {int_or_none}),
|
||||
}),
|
||||
}
|
@@ -102,11 +102,10 @@ class MedalTVIE(InfoExtractor):
|
||||
item_id = item_id or '%dp' % height
|
||||
if item_id not in item_url:
|
||||
return
|
||||
width = int(round(aspect_ratio * height))
|
||||
container.append({
|
||||
'url': item_url,
|
||||
id_key: item_id,
|
||||
'width': width,
|
||||
'width': round(aspect_ratio * height),
|
||||
'height': height,
|
||||
})
|
||||
|
||||
|
@@ -1,5 +1,7 @@
|
||||
from .telecinco import TelecincoBaseIE
|
||||
from ..networking.exceptions import HTTPError
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
int_or_none,
|
||||
parse_iso8601,
|
||||
)
|
||||
@@ -79,7 +81,17 @@ class MiTeleIE(TelecincoBaseIE):
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
|
||||
try: # yt-dlp's default user-agents are too old and blocked by akamai
|
||||
webpage = self._download_webpage(url, display_id, headers={
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:136.0) Gecko/20100101 Firefox/136.0',
|
||||
})
|
||||
except ExtractorError as e:
|
||||
if not isinstance(e.cause, HTTPError) or e.cause.status != 403:
|
||||
raise
|
||||
# Retry with impersonation if hardcoded UA is insufficient to bypass akamai
|
||||
webpage = self._download_webpage(url, display_id, impersonate=True)
|
||||
|
||||
pre_player = self._search_json(
|
||||
r'window\.\$REACTBASE_STATE\.prePlayer_mtweb\s*=',
|
||||
webpage, 'Pre Player', display_id)['prePlayer']
|
||||
|
@@ -46,7 +46,7 @@ class TelecincoBaseIE(InfoExtractor):
|
||||
error_code = traverse_obj(
|
||||
self._webpage_read_content(error.cause.response, caronte['cerbero'], video_id, fatal=False),
|
||||
({json.loads}, 'code', {int}))
|
||||
if error_code == 4038:
|
||||
if error_code in (4038, 40313):
|
||||
self.raise_geo_restricted(countries=['ES'])
|
||||
raise
|
||||
|
||||
|
@@ -201,7 +201,35 @@ class VrtNUIE(VRTBaseIE):
|
||||
'timestamp': 1740373200,
|
||||
'title': 'Reeks 6 volledig vanaf 3 maart',
|
||||
'upload_date': '20250224',
|
||||
'_old_archive_ids': ['canvas pbs-pub-c8a78645-5d3e-468a-89ec-6f3ed5534bd5$vid-242ddfe9-18f5-4e16-ab45-09b122a19251'],
|
||||
'_old_archive_ids': [
|
||||
'canvas pbs-pub-c8a78645-5d3e-468a-89ec-6f3ed5534bd5$vid-242ddfe9-18f5-4e16-ab45-09b122a19251',
|
||||
'ketnet pbs-pub-c8a78645-5d3e-468a-89ec-6f3ed5534bd5$vid-242ddfe9-18f5-4e16-ab45-09b122a19251',
|
||||
],
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.vrt.be/vrtmax/a-z/meisjes/6/meisjes-s6a5/',
|
||||
'info_dict': {
|
||||
'id': 'pbs-pub-97b541ab-e05c-43b9-9a40-445702ef7189$vid-5e306921-a9aa-4fa9-9f39-5b82c8f1028e',
|
||||
'ext': 'mp4',
|
||||
'channel': 'ketnet',
|
||||
'description': 'md5:713793f15cbf677f66200b36b7b1ec5a',
|
||||
'display_id': 'meisjes-s6a5',
|
||||
'duration': 1336.02,
|
||||
'episode': 'Week 5',
|
||||
'episode_id': '1684157692901',
|
||||
'episode_number': 5,
|
||||
'season': '6',
|
||||
'season_id': '1684157692901',
|
||||
'season_number': 6,
|
||||
'series': 'Meisjes',
|
||||
'thumbnail': 'https://images.vrt.be/orig/2023/05/14/bf526ae0-f1d9-11ed-91d7-02b7b76bf47f.jpg',
|
||||
'timestamp': 1685251800,
|
||||
'title': 'Week 5',
|
||||
'upload_date': '20230528',
|
||||
'_old_archive_ids': [
|
||||
'canvas pbs-pub-97b541ab-e05c-43b9-9a40-445702ef7189$vid-5e306921-a9aa-4fa9-9f39-5b82c8f1028e',
|
||||
'ketnet pbs-pub-97b541ab-e05c-43b9-9a40-445702ef7189$vid-5e306921-a9aa-4fa9-9f39-5b82c8f1028e',
|
||||
],
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.vrt.be/vrtnu/a-z/taboe/3/taboe-s3a4/',
|
||||
@@ -223,7 +251,10 @@ class VrtNUIE(VRTBaseIE):
|
||||
'timestamp': 1740286800,
|
||||
'title': 'Mensen met het syndroom van Gilles de la Tourette',
|
||||
'upload_date': '20250223',
|
||||
'_old_archive_ids': ['canvas pbs-pub-f50faa3a-1778-46b6-9117-4ba85f197703$vid-547507fe-1c8b-4394-b361-21e627cbd0fd'],
|
||||
'_old_archive_ids': [
|
||||
'canvas pbs-pub-f50faa3a-1778-46b6-9117-4ba85f197703$vid-547507fe-1c8b-4394-b361-21e627cbd0fd',
|
||||
'ketnet pbs-pub-f50faa3a-1778-46b6-9117-4ba85f197703$vid-547507fe-1c8b-4394-b361-21e627cbd0fd',
|
||||
],
|
||||
},
|
||||
}]
|
||||
_NETRC_MACHINE = 'vrtnu'
|
||||
@@ -427,66 +458,8 @@ class VrtNUIE(VRTBaseIE):
|
||||
'display_id': display_id,
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
'_old_archive_ids': [make_archive_id('Canvas', video_id)],
|
||||
}
|
||||
|
||||
|
||||
class KetnetIE(VRTBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?ketnet\.be/(?P<id>(?:[^/]+/)*[^/?#&]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.ketnet.be/kijken/m/meisjes/6/meisjes-s6a5',
|
||||
'info_dict': {
|
||||
'id': 'pbs-pub-39f8351c-a0a0-43e6-8394-205d597d6162$vid-5e306921-a9aa-4fa9-9f39-5b82c8f1028e',
|
||||
'ext': 'mp4',
|
||||
'title': 'Meisjes',
|
||||
'episode': 'Reeks 6: Week 5',
|
||||
'season': 'Reeks 6',
|
||||
'series': 'Meisjes',
|
||||
'timestamp': 1685251800,
|
||||
'upload_date': '20230528',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
|
||||
video = self._download_json(
|
||||
'https://senior-bff.ketnet.be/graphql', display_id, query={
|
||||
'query': '''{
|
||||
video(id: "content/ketnet/nl/%s.model.json") {
|
||||
description
|
||||
episodeNr
|
||||
imageUrl
|
||||
mediaReference
|
||||
programTitle
|
||||
publicationDate
|
||||
seasonTitle
|
||||
subtitleVideodetail
|
||||
titleVideodetail
|
||||
}
|
||||
}''' % display_id, # noqa: UP031
|
||||
})['data']['video']
|
||||
|
||||
video_id = urllib.parse.unquote(video['mediaReference'])
|
||||
data = self._call_api(video_id, 'ketnet@PROD', version='v1')
|
||||
formats, subtitles = self._extract_formats_and_subtitles(data, video_id)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
'_old_archive_ids': [make_archive_id('Canvas', video_id)],
|
||||
**traverse_obj(video, {
|
||||
'title': ('titleVideodetail', {str}),
|
||||
'description': ('description', {str}),
|
||||
'thumbnail': ('thumbnail', {url_or_none}),
|
||||
'timestamp': ('publicationDate', {parse_iso8601}),
|
||||
'series': ('programTitle', {str}),
|
||||
'season': ('seasonTitle', {str}),
|
||||
'episode': ('subtitleVideodetail', {str}),
|
||||
'episode_number': ('episodeNr', {int_or_none}),
|
||||
}),
|
||||
'_old_archive_ids': [make_archive_id('Canvas', video_id),
|
||||
make_archive_id('Ketnet', video_id)],
|
||||
}
|
||||
|
||||
|
||||
|
@@ -109,7 +109,7 @@ class WeiboBaseIE(InfoExtractor):
|
||||
**traverse_obj(video_info, {
|
||||
'display_id': ('mblogid', {str_or_none}),
|
||||
'title': ('page_info', 'media_info', ('video_title', 'kol_title', 'name'),
|
||||
{lambda x: x.replace('\n', ' ')}, {truncate_string(left=50)}, filter),
|
||||
{lambda x: x.replace('\n', ' ')}, {truncate_string(left=72)}, filter),
|
||||
'alt_title': ('page_info', 'media_info', ('video_title', 'kol_title', 'name'), {str}, filter),
|
||||
'description': ('text_raw', {str}),
|
||||
'duration': ('page_info', 'media_info', 'duration', {int_or_none}),
|
||||
@@ -213,6 +213,7 @@ class WeiboVideoIE(WeiboBaseIE):
|
||||
'ext': 'mp4',
|
||||
'display_id': 'LEZDodaiW',
|
||||
'title': '呃,稍微了解了一下靡烟miya,感觉这东西也太二了',
|
||||
'alt_title': '呃,稍微了解了一下靡烟miya,感觉这东西也太二了',
|
||||
'description': '呃,稍微了解了一下靡烟miya,感觉这东西也太二了 http://t.cn/A6aerGsM \u200b\u200b\u200b',
|
||||
'duration': 76,
|
||||
'timestamp': 1659344278,
|
||||
@@ -224,6 +225,7 @@ class WeiboVideoIE(WeiboBaseIE):
|
||||
'view_count': int,
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'_old_archive_ids': ['weibomobile 4797700463137878'],
|
||||
},
|
||||
}]
|
||||
|
||||
|
@@ -130,7 +130,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
_RETURN_TYPE = 'video' # XXX: How to handle multifeed?
|
||||
|
||||
_PLAYER_INFO_RE = (
|
||||
r'/s/player/(?P<id>[a-zA-Z0-9_-]{8,})/player',
|
||||
r'/s/player/(?P<id>[a-zA-Z0-9_-]{8,})/(?:tv-)?player',
|
||||
r'/(?P<id>[a-zA-Z0-9_-]{8,})/player(?:_ias\.vflset(?:/[a-zA-Z]{2,3}_[a-zA-Z]{2,3})?|-plasma-ias-(?:phone|tablet)-[a-z]{2}_[A-Z]{2}\.vflset)/base\.js$',
|
||||
r'\b(?P<id>vfl[a-zA-Z0-9_-]+)\b.*?\.js$',
|
||||
)
|
||||
@@ -1939,11 +1939,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
get_all=False, expected_type=str)
|
||||
if not player_url:
|
||||
return
|
||||
# TODO: Add proper support for the 'tce' variant players
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/12398
|
||||
if '/player_ias_tce.vflset/' in player_url:
|
||||
self.write_debug(f'Modifying tce player URL: {player_url}')
|
||||
player_url = player_url.replace('/player_ias_tce.vflset/', '/player_ias.vflset/')
|
||||
return urljoin('https://www.youtube.com', player_url)
|
||||
|
||||
def _download_player_url(self, video_id, fatal=False):
|
||||
@@ -2069,7 +2064,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
jscode, 'Initial JS player signature function name', group='sig')
|
||||
|
||||
jsi = JSInterpreter(jscode)
|
||||
initial_function = jsi.extract_function(funcname)
|
||||
global_var_map = {}
|
||||
_, varname, value = self._extract_player_js_global_var(jscode)
|
||||
if varname:
|
||||
global_var_map[varname] = jsi.interpret_expression(value, {}, allow_recursion=100)
|
||||
initial_function = jsi.extract_function(funcname, global_var_map)
|
||||
return lambda s: initial_function([s])
|
||||
|
||||
def _cached(self, func, *cache_id):
|
||||
@@ -2173,14 +2172,31 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
rf'var {re.escape(funcname)}\s*=\s*(\[.+?\])\s*[,;]', jscode,
|
||||
f'Initial JS player n function list ({funcname}.{idx})')))[int(idx)]
|
||||
|
||||
def _fixup_n_function_code(self, argnames, code):
|
||||
def _extract_player_js_global_var(self, jscode):
|
||||
"""Returns tuple of strings: variable assignment code, variable name, variable value code"""
|
||||
return self._search_regex(
|
||||
r'''(?x)
|
||||
\'use\s+strict\';\s*
|
||||
(?P<code>
|
||||
var\s+(?P<name>[a-zA-Z0-9_$]+)\s*=\s*
|
||||
(?P<value>"(?:[^"\\]|\\.)+"\.split\("[^"]+"\))
|
||||
)[;,]
|
||||
''', jscode, 'global variable', group=('code', 'name', 'value'), default=(None, None, None))
|
||||
|
||||
def _fixup_n_function_code(self, argnames, code, full_code):
|
||||
global_var, varname, _ = self._extract_player_js_global_var(full_code)
|
||||
if global_var:
|
||||
self.write_debug(f'Prepending n function code with global array variable "{varname}"')
|
||||
code = global_var + ', ' + code
|
||||
else:
|
||||
self.write_debug('No global array variable found in player JS')
|
||||
return argnames, re.sub(
|
||||
rf';\s*if\s*\(\s*typeof\s+[a-zA-Z0-9_$]+\s*===?\s*(["\'])undefined\1\s*\)\s*return\s+{argnames[0]};',
|
||||
rf';\s*if\s*\(\s*typeof\s+[a-zA-Z0-9_$]+\s*===?\s*(?:(["\'])undefined\1|{varname}\[\d+\])\s*\)\s*return\s+{argnames[0]};',
|
||||
';', code)
|
||||
|
||||
def _extract_n_function_code(self, video_id, player_url):
|
||||
player_id = self._extract_player_info(player_url)
|
||||
func_code = self.cache.load('youtube-nsig', player_id, min_ver='2025.02.19')
|
||||
func_code = self.cache.load('youtube-nsig', player_id, min_ver='2025.03.21')
|
||||
jscode = func_code or self._load_player(video_id, player_url)
|
||||
jsi = JSInterpreter(jscode)
|
||||
|
||||
@@ -2189,8 +2205,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
func_name = self._extract_n_function_name(jscode, player_url=player_url)
|
||||
|
||||
# XXX: Workaround for the `typeof` gotcha
|
||||
func_code = self._fixup_n_function_code(*jsi.extract_function_code(func_name))
|
||||
# XXX: Workaround for the global array variable and lack of `typeof` implementation
|
||||
func_code = self._fixup_n_function_code(*jsi.extract_function_code(func_name), jscode)
|
||||
|
||||
self.cache.store('youtube-nsig', player_id, func_code)
|
||||
return jsi, player_id, func_code
|
||||
@@ -3141,14 +3157,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'n': decrypt_nsig(query['n'][0], video_id, player_url),
|
||||
})
|
||||
except ExtractorError as e:
|
||||
phantomjs_hint = ''
|
||||
if isinstance(e, JSInterpreter.Exception):
|
||||
phantomjs_hint = (f' Install {self._downloader._format_err("PhantomJS", self._downloader.Styles.EMPHASIS)} '
|
||||
f'to workaround the issue. {PhantomJSwrapper.INSTALL_HINT}\n')
|
||||
if player_url:
|
||||
self.report_warning(
|
||||
f'nsig extraction failed: Some formats may be missing\n{phantomjs_hint}'
|
||||
f' n = {query["n"][0]} ; player = {player_url}', video_id=video_id, only_once=True)
|
||||
f'nsig extraction failed: Some formats may be missing\n'
|
||||
f' n = {query["n"][0]} ; player = {player_url}',
|
||||
video_id=video_id, only_once=True)
|
||||
self.write_debug(e, only_once=True)
|
||||
else:
|
||||
self.report_warning(
|
||||
|
@@ -301,7 +301,7 @@ class JSInterpreter:
|
||||
OP_CHARS = '+-*/%&|^=<>!,;{}:['
|
||||
if not expr:
|
||||
return
|
||||
counters = {k: 0 for k in _MATCHING_PARENS.values()}
|
||||
counters = dict.fromkeys(_MATCHING_PARENS.values(), 0)
|
||||
start, splits, pos, delim_len = 0, 0, 0, len(delim) - 1
|
||||
in_quote, escaping, after_op, in_regex_char_group = None, False, True, False
|
||||
for idx, char in enumerate(expr):
|
||||
@@ -890,9 +890,9 @@ class JSInterpreter:
|
||||
code, _ = self._separate_at_paren(func_m.group('code'))
|
||||
return [x.strip() for x in func_m.group('args').split(',')], code
|
||||
|
||||
def extract_function(self, funcname):
|
||||
def extract_function(self, funcname, *global_stack):
|
||||
return function_with_repr(
|
||||
self.extract_function_from_code(*self.extract_function_code(funcname)),
|
||||
self.extract_function_from_code(*self.extract_function_code(funcname), *global_stack),
|
||||
f'F<{funcname}>')
|
||||
|
||||
def extract_function_from_code(self, argnames, code, *global_stack):
|
||||
|
@@ -743,7 +743,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
|
||||
if value not in ('', None):
|
||||
value = ', '.join(map(str, variadic(value)))
|
||||
value = value.replace('\0', '') # nul character cannot be passed in command line
|
||||
metadata['common'].update({meta_f: value for meta_f in variadic(meta_list)})
|
||||
metadata['common'].update(dict.fromkeys(variadic(meta_list), value))
|
||||
|
||||
# Info on media metadata/metadata supported by ffmpeg:
|
||||
# https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
|
||||
|
@@ -117,7 +117,7 @@ _FILE_SUFFIXES = {
|
||||
}
|
||||
|
||||
_NON_UPDATEABLE_REASONS = {
|
||||
**{variant: None for variant in _FILE_SUFFIXES}, # Updatable
|
||||
**dict.fromkeys(_FILE_SUFFIXES), # Updatable
|
||||
**{variant: f'Auto-update is not supported for unpackaged {name} executable; Re-download the latest release'
|
||||
for variant, name in {'win32_dir': 'Windows', 'darwin_dir': 'MacOS', 'linux_dir': 'Linux'}.items()},
|
||||
'py2exe': 'py2exe is no longer supported by yt-dlp; This executable cannot be updated',
|
||||
|
@@ -3247,7 +3247,7 @@ def _match_one(filter_part, dct, incomplete):
|
||||
op = lambda attr, value: not unnegated_op(attr, value)
|
||||
else:
|
||||
op = unnegated_op
|
||||
comparison_value = m['quotedstrval'] or m['strval'] or m['intval']
|
||||
comparison_value = m['quotedstrval'] or m['strval']
|
||||
if m['quote']:
|
||||
comparison_value = comparison_value.replace(r'\{}'.format(m['quote']), m['quote'])
|
||||
actual_value = dct.get(m['key'])
|
||||
|
@@ -1,8 +1,8 @@
|
||||
# Autogenerated by devscripts/update-version.py
|
||||
|
||||
__version__ = '2025.02.19'
|
||||
__version__ = '2025.03.21'
|
||||
|
||||
RELEASE_GIT_HEAD = '4985a4041770eaa0016271809a1fd950dc809a55'
|
||||
RELEASE_GIT_HEAD = 'f36e4b6e65cb8403791aae2f520697115cb88dec'
|
||||
|
||||
VARIANT = None
|
||||
|
||||
@@ -12,4 +12,4 @@ CHANNEL = 'stable'
|
||||
|
||||
ORIGIN = 'yt-dlp/yt-dlp'
|
||||
|
||||
_pkg_version = '2025.02.19'
|
||||
_pkg_version = '2025.03.21'
|
||||
|
Reference in New Issue
Block a user