Update On Tue Mar 25 19:36:01 CET 2025

This commit is contained in:
github-action[bot]
2025-03-25 19:36:01 +01:00
parent 12dec4d1bf
commit d4f83848de
80 changed files with 2335 additions and 1260 deletions
+1
View File
@@ -952,3 +952,4 @@ Update On Fri Mar 21 19:35:22 CET 2025
Update On Sat Mar 22 19:34:51 CET 2025
Update On Sun Mar 23 19:33:38 CET 2025
Update On Mon Mar 24 19:37:20 CET 2025
Update On Tue Mar 25 19:35:52 CET 2025
+376
View File
@@ -0,0 +1,376 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Rider
.idea
# macOS shit
.DS_Store
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# debug log
debug_*.json
# dotnet run in `BBDown/` sub folder
/BBDown/*.mp4
/BBDown/*.xml
/BBDown/*.ass
+1 -1
View File
@@ -162,7 +162,7 @@ public static partial class Parser
return parsedResult;
}
// data节点一次性判断完
string nodeName = null;
string? nodeName = null;
if (parsedResult.WebJsonString.Contains("\"result\":{"))
{
nodeName = "result";
+1 -1
View File
@@ -71,7 +71,7 @@ public static class HTTPUtil
return location;
}
public static async Task<byte[]> GetPostResponseAsync(string Url, byte[] postData, Dictionary<string, string> headers = null)
public static async Task<byte[]> GetPostResponseAsync(string Url, byte[] postData, Dictionary<string, string>? headers = null)
{
LogDebug("Post to: {0}, data: {1}", Url, Convert.ToBase64String(postData));
+43 -7
View File
@@ -2,6 +2,9 @@ using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
@@ -54,7 +57,7 @@ public class BBDownApiServer
}
return Results.Json(task, AppJsonSerializerContext.Default.DownloadTask);
});
app.MapPost("/add-task", (MyOptionBindingResult<MyOption> bindingResult) =>
app.MapPost("/add-task", (MyOptionBindingResult<ServeRequestOptions> bindingResult) =>
{
if (!bindingResult.IsValid)
{
@@ -62,7 +65,26 @@ public class BBDownApiServer
return Results.BadRequest("输入有误");
}
var req = bindingResult.Result;
_ = AddDownloadTaskAsync(req);
_ = AddDownloadTaskAsync(req)
.ContinueWith(async task => {
// send request to callback webhook
if (string.IsNullOrEmpty(req.CallBackWebHook))
{
return;
}
string callback = req.CallBackWebHook;
var client = new HttpClient();
var downloadTask = await task;
string? jsonContent = JsonSerializer.Serialize(downloadTask, AppJsonSerializerContext.Default.DownloadTask);
try
{
await client.PostAsync(callback, new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json"));
}
catch (System.Exception e)
{
Logger.LogDebug("回调失败", e.Message);
}
});
return Results.Ok();
});
var finishedRemovalApi = app.MapGroup("remove-finished");
@@ -90,10 +112,14 @@ public class BBDownApiServer
app.Run(url);
}
private async Task AddDownloadTaskAsync(MyOption option)
private async Task<DownloadTask> AddDownloadTaskAsync(MyOption option)
{
var aid = await BBDownUtil.GetAvIdAsync(option.Url);
if (runningTasks.Any(task => task.Aid == aid)) return;
DownloadTask? runningTask = runningTasks.FirstOrDefault(task => task.Aid == aid);
if (runningTask is not null)
{
return runningTask;
};
var task = new DownloadTask(aid, option.Url, DateTimeOffset.Now.ToUnixTimeSeconds());
runningTasks.Add(task);
try
@@ -125,6 +151,7 @@ public class BBDownApiServer
}
runningTasks.Remove(task);
finishedTasks.Add(task);
return task;
}
}
@@ -146,6 +173,9 @@ public record DownloadTask(string Aid, string Url, long TaskCreateTime)
public double TotalDownloadedBytes = 0f;
[JsonInclude]
public bool IsSuccessful = false;
[JsonInclude]
public List<string> SavePaths = new();
};
public record DownloadTaskCollection(List<DownloadTask> Running, List<DownloadTask> Finished);
@@ -153,15 +183,20 @@ record struct MyOptionBindingResult<T>(T? Result, Exception? Exception)
{
public bool IsValid => Exception is null;
public static async ValueTask<MyOptionBindingResult<MyOption>> BindAsync(HttpContext httpContext)
public static async ValueTask<MyOptionBindingResult<T>> BindAsync(HttpContext httpContext)
{
try
{
var item = await httpContext.Request.ReadFromJsonAsync(SourceGenerationContext.Default.MyOption);
JsonTypeInfo? jsonTypeInfo = SourceGenerationContext.Default.GetTypeInfo(typeof(T));
if (jsonTypeInfo is null)
{
return new(default, new InvalidOperationException($"Cannot find TypeInfo for type {typeof(T)}"));
}
var item = await httpContext.Request.ReadFromJsonAsync(jsonTypeInfo);
if (item is null) return new(default, new NoNullAllowedException());
return new(item, null);
return new((T)item, null);
}
catch (Exception ex)
{
@@ -182,6 +217,7 @@ public partial class AppJsonSerializerContext : JsonSerializerContext
}
[JsonSerializable(typeof(MyOption))]
[JsonSerializable(typeof(ServeRequestOptions))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
@@ -0,0 +1,11 @@
using BBDown;
internal class ServeRequestOptions : MyOption
{
/// <summary>
/// 任务完成回调Http请求地址
/// </summary>
public string? CallBackWebHook { get; set; }
}
+9 -2
View File
@@ -49,6 +49,7 @@ partial class Program
}
[JsonSerializable(typeof(MyOption))]
[JsonSerializable(typeof(ServeRequestOptions))]
partial class MyOptionJsonContext : JsonSerializerContext { }
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e)
@@ -417,7 +418,7 @@ partial class Program
}
if (!myOption.SkipCover && !myOption.SubOnly && !File.Exists(coverPath) && !myOption.DanmakuOnly && !myOption.CoverOnly)
{
await DownloadFileAsync((pic == "" ? p.cover! : pic), coverPath, new DownloadConfig());
await DownloadFileAsync(pic == "" ? p.cover! : pic, coverPath, new DownloadConfig());
}
if (!myOption.SkipSubtitle && !myOption.DanmakuOnly && !myOption.CoverOnly)
@@ -598,7 +599,7 @@ partial class Program
var newCoverPath = Path.ChangeExtension(savePath, Path.GetExtension(coverUrl));
await DownloadFileAsync(coverUrl, newCoverPath, downloadConfig);
if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true);
return;
relatedTask?.SavePaths.Add(newCoverPath);
}
Log($"已选择的流:");
@@ -616,6 +617,7 @@ partial class Program
if (!myOption.OnlyShowInfo && File.Exists(savePath) && new FileInfo(savePath).Length != 0)
{
Log($"{savePath}已存在, 跳过下载...");
relatedTask?.SavePaths.Add(savePath);
File.Delete(coverPath);
if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0)
{
@@ -735,6 +737,7 @@ partial class Program
if (File.Exists(savePath) && new FileInfo(savePath).Length != 0)
{
Log($"{savePath}已存在, 跳过下载...");
relatedTask?.SavePaths.Add(savePath);
if (selectedPagesInfo.Count == 1 && Directory.Exists(p.aid))
{
Directory.Delete(p.aid, true);
@@ -789,6 +792,10 @@ partial class Program
}
LogDebug("{0}", parsedResult.WebJsonString);
}
if (!string.IsNullOrWhiteSpace(savePath)) {
relatedTask?.SavePaths.Add(savePath);
}
}
catch (Exception ex)
{
+22
View File
@@ -0,0 +1,22 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
WORKDIR /app
COPY . .
RUN dotnet build -c Release
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=builder /app/BBDown/bin/Release/net9.0 .
EXPOSE 23333
# install ffmpeg
RUN apt-get update && \
apt-get install -y ffmpeg && \
chmod +x /app/BBDown
ENTRYPOINT ["/app/BBDown", "serve", "-l", "http://0.0.0.0:23333"]
+1 -1
View File
@@ -301,7 +301,7 @@ BBDown -p ALL "https://www.bilibili.com/bangumi/play/ss33073"
启动服务器(自定义监听地址和端口):
```shell
BBDown server -l http://0.0.0.0:12450
BBDown serve -l http://0.0.0.0:12450
```
API服务器不支持HTTPS配置,如果有需要请自行使用nginx等反向代理进行配置
+2 -2
View File
@@ -135,10 +135,10 @@ curl -X POST -H 'Content-Type: application/json' -d '{ "Url": "BV1qt4y1X7TW" }'
Windows:
```shell
curl -X POST -H 'Content-Type: application/json' -d { "Url": "BV1qt4y1X7TW", "FilePattern": "C:\\Downloads\\<videoTitle>[<dfn>]" }' http://localhost:58682/add-task
curl -X POST -H 'Content-Type: application/json' -d '{ "Url": "BV1qt4y1X7TW", "FilePattern": "C:\\Downloads\\<videoTitle>[<dfn>]" }' http://localhost:58682/add-task
```
Unix-Like:
```shell
curl -X POST -H 'Content-Type: application/json' -d { "Url": "BV1qt4y1X7TW", "FilePattern": "/Downloads/<videoTitle>[<dfn>]" }' http://localhost:58682/add-task
curl -X POST -H 'Content-Type: application/json' -d '{ "Url": "BV1qt4y1X7TW", "FilePattern": "/Downloads/<videoTitle>[<dfn>]" }' http://localhost:58682/add-task
```
+2 -2
View File
@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.3",
"mihomo_alpha": "alpha-0f32c05",
"mihomo_alpha": "alpha-7b38261",
"clash_rs": "v0.7.6",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.7.6-alpha+sha.153fb70"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
}
},
"updated_at": "2025-03-20T22:20:42.605Z"
"updated_at": "2025-03-24T22:21:04.112Z"
}
+31 -5
View File
@@ -1,4 +1,13 @@
## v2.2.1
## v2.2.3-alpha
#### 已知问题
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
### 2.2.3-alpha 相对于 2.2.2
#### 优化
- 重构了内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性
## v2.2.2
**发行代号:拓**
@@ -6,8 +15,24 @@
代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。
2.2.1 相对于 2.2.0(已下架不在提供)
修复了:
#### 已知问题
- 仅在Ubuntu 22.04/24.04Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优
### 2.2.2 相对于 2.2.1(已下架不在提供)
#### 修复了:
- 弹黑框的问题(原因是服务崩溃触发重装机制)
- MacOS进入轻量模式以后影藏Dock图标
- 增加轻量模式缺失的tray翻译
- Linux下的窗口边框被削掉的问题
#### 新增了:
- 加强服务检测和重装逻辑
- 增强内核与服务保活机制
- 增加服务模式下的僵尸进程清理机制
- 新增当服务模式多次尝试失败后自动回退至用户空间模式
### 2.2.1 相对于 2.2.0(已下架不在提供)
#### 修复了:
1. **首页**
- 修复 Direct 模式首页无法渲染
- 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出
@@ -23,7 +48,7 @@
4. **轻量模式**
- 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。
新增了:
#### 新增了:
1. **首页**
- 首页文本过长自动截断
2. **轻量模式**
@@ -37,7 +62,7 @@
---
## v2.2.0(已下架不在提供)
## 2.2.0(已下架不在提供)
#### 新增功能
1. **首页**
@@ -78,6 +103,7 @@
- 修复 macOS tray图标错位到左上角的问题。
- 修复 Windows/Linux 运行时崩溃。
- 修复 Win10 阴影和边框问题。
- 修复 升级或重装后开机自启状态检测和同步问题。
2. **构建**
- 修复构建失败问题。
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.2.1",
"version": "v2.2.3-alpha",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev -- --profile fast-dev",
@@ -17,6 +17,7 @@
"portable": "node scripts/portable.mjs",
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"fix-alpha-version": "node scripts/fix-alpha_version.mjs",
"release-version": "node scripts/release_version.mjs",
"release-alpha-version": "node scripts/release-alpha_version.mjs",
"prepare": "husky",
"clean": "cd ./src-tauri && cargo clean && cd -"
@@ -109,4 +110,4 @@
},
"type": "module",
"packageManager": "pnpm@9.13.2"
}
}
+195
View File
@@ -0,0 +1,195 @@
import fs from "fs/promises";
import path from "path";
import { program } from "commander";
/**
* 验证版本号格式
* @param {string} version
* @returns {boolean}
*/
function isValidVersion(version) {
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?$/i.test(version);
}
/**
* 标准化版本号(确保v前缀可选)
* @param {string} version
* @returns {string}
*/
function normalizeVersion(version) {
return version.startsWith("v") ? version : `v${version}`;
}
/**
* 更新 package.json 文件中的版本号
* @param {string} newVersion 新版本号
*/
async function updatePackageVersion(newVersion) {
const _dirname = process.cwd();
const packageJsonPath = path.join(_dirname, "package.json");
try {
const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data);
console.log(
"[INFO]: Current package.json version is: ",
packageJson.version,
);
packageJson.version = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
"utf8",
);
console.log(
`[INFO]: package.json version updated to: ${packageJson.version}`,
);
return packageJson.version;
} catch (error) {
console.error("Error updating package.json version:", error);
throw error;
}
}
/**
* 更新 Cargo.toml 文件中的版本号
* @param {string} newVersion 新版本号
*/
async function updateCargoVersion(newVersion) {
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 versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
const updatedLines = lines.map((line) => {
if (line.trim().startsWith("version =")) {
return line.replace(
/version\s*=\s*"[^"]+"/,
`version = "${versionWithoutV}"`,
);
}
return line;
});
await fs.writeFile(cargoTomlPath, updatedLines.join("\n"), "utf8");
console.log(`[INFO]: Cargo.toml version updated to: ${versionWithoutV}`);
} catch (error) {
console.error("Error updating Cargo.toml version:", error);
throw error;
}
}
/**
* 更新 tauri.conf.json 文件中的版本号
* @param {string} newVersion 新版本号
*/
async function updateTauriConfigVersion(newVersion) {
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);
const versionWithoutV = newVersion.startsWith("v")
? newVersion.slice(1)
: newVersion;
console.log(
"[INFO]: Current tauri.conf.json version is: ",
tauriConfig.version,
);
tauriConfig.version = versionWithoutV;
await fs.writeFile(
tauriConfigPath,
JSON.stringify(tauriConfig, null, 2),
"utf8",
);
console.log(
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
);
} catch (error) {
console.error("Error updating tauri.conf.json version:", error);
throw error;
}
}
/**
* 获取当前版本号(从package.json
*/
async function getCurrentVersion() {
const _dirname = process.cwd();
const packageJsonPath = path.join(_dirname, "package.json");
try {
const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data);
return packageJson.version;
} catch (error) {
console.error("Error getting current version:", error);
throw error;
}
}
/**
* 主函数,更新所有文件的版本号
* @param {string} versionArg 版本参数(可以是标签或完整版本号)
*/
async function main(versionArg) {
if (!versionArg) {
console.error("Error: Version argument is required");
process.exit(1);
}
try {
let newVersion;
const validTags = ["alpha", "beta", "rc"];
// 判断参数是标签还是完整版本号
if (validTags.includes(versionArg.toLowerCase())) {
// 标签模式:在当前版本基础上添加标签
const currentVersion = await getCurrentVersion();
const baseVersion = currentVersion.replace(
/-(alpha|beta|rc)(\.\d+)?$/i,
"",
);
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
} else {
// 完整版本号模式
if (!isValidVersion(versionArg)) {
console.error(
"Error: Invalid version format. Expected format: vX.X.X or vX.X.X-tag (e.g. v2.2.3 or v2.2.3-alpha)",
);
process.exit(1);
}
newVersion = normalizeVersion(versionArg);
}
console.log(`[INFO]: Updating versions to: ${newVersion}`);
await updatePackageVersion("v" + newVersion);
await updateCargoVersion(newVersion);
await updateTauriConfigVersion(newVersion);
console.log("[SUCCESS]: All version updates completed successfully!");
} catch (error) {
console.error("[ERROR]: Failed to update versions:", error);
process.exit(1);
}
}
// 设置命令行界面
program
.name("pnpm release-version")
.description(
"Update project version numbers. Can add tag (alpha/beta/rc) or set full version (e.g. v2.2.3 or v2.2.3-alpha)",
)
.argument(
"<version>",
"version tag (alpha/beta/rc) or full version (e.g. v2.2.3 or v2.2.3-alpha)",
)
.action(main)
.parse(process.argv);
+1 -1
View File
@@ -1132,7 +1132,7 @@ dependencies = [
[[package]]
name = "clash-verge"
version = "2.2.1"
version = "2.2.3-alpha"
dependencies = [
"ab_glyph",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.2.1"
version = "2.2.3-alpha"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
+1 -3
View File
@@ -1,9 +1,8 @@
use super::CmdResult;
use crate::{core::CoreManager, module::mihomo::MihomoManager};
use crate::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()
@@ -15,7 +14,6 @@ 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()
+1 -1
View File
@@ -47,7 +47,7 @@ pub async fn get_system_info() -> CmdResult<String> {
pub async fn get_running_mode() -> Result<String, String> {
match CoreManager::global().get_running_mode().await {
core::RunningMode::Service => Ok("service".to_string()),
core::RunningMode::Sidecar => Ok("sidecar".to_string()),
core::RunningMode::Sidecar => Ok("standalone".to_string()),
core::RunningMode::NotRunning => Ok("not_running".to_string()),
}
}
File diff suppressed because it is too large Load Diff
@@ -3,15 +3,11 @@ use once_cell::sync::OnceCell;
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 {
@@ -21,8 +17,6 @@ impl Handle {
HANDLE.get_or_init(|| 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)),
})
}
@@ -74,39 +68,7 @@ impl Handle {
*is_exiting = true;
}
pub fn set_core_process(&self, process: CommandChild) {
let mut core_process = self.core_process.write();
*core_process = Some(process);
}
pub fn take_core_process(&self) -> Option<CommandChild> {
let mut core_process = self.core_process.write();
core_process.take()
}
/// 检查是否有运行中的核心进程
pub fn has_core_process(&self) -> bool {
self.core_process.read().is_some()
}
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()
}
}
+113 -50
View File
@@ -1,6 +1,9 @@
use crate::{
config::Config, core::handle, feat, log_err, module::lightweight::entry_lightweight_mode,
utils::resolve,
config::Config,
core::handle,
feat, log_err, logging,
module::lightweight::entry_lightweight_mode,
utils::{logging::Type, resolve},
};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
@@ -26,22 +29,27 @@ impl Hotkey {
let verge = Config::verge();
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
println!(
"Initializing hotkeys, global hotkey enabled: {}",
logging!(
info,
Type::Hotkey,
true,
"Initializing hotkeys with enable: {}",
enable_global_hotkey
);
log::info!(target: "app", "Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
// 如果全局热键被禁用,则不注册热键
if !enable_global_hotkey {
println!("Global hotkey is disabled, skipping registration");
log::info!(target: "app", "Global hotkey is disabled, skipping registration");
return Ok(());
}
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
println!("Found {} hotkeys to register", hotkeys.len());
log::info!(target: "app", "Found {} hotkeys to register", hotkeys.len());
logging!(
info,
Type::Hotkey,
true,
"Has {} hotkeys need to register",
hotkeys.len()
);
for hotkey in hotkeys.iter() {
let mut iter = hotkey.split(',');
@@ -50,28 +58,52 @@ impl Hotkey {
match (key, func) {
(Some(key), Some(func)) => {
println!("Registering hotkey: {} -> {}", key, func);
log::info!(target: "app", "Registering hotkey: {} -> {}", key, func);
logging!(
info,
Type::Hotkey,
true,
"Registering hotkey: {} -> {}",
key,
func
);
if let Err(e) = self.register(key, func) {
println!("Failed to register hotkey {} -> {}: {:?}", key, func, e);
log::error!(target: "app", "Failed to register hotkey {} -> {}: {:?}", key, func, e);
logging!(
error,
Type::Hotkey,
true,
"Failed to register hotkey {} -> {}: {:?}",
key,
func,
e
);
} else {
println!("Successfully registered hotkey {} -> {}", key, func);
log::info!(target: "app", "Successfully registered hotkey {} -> {}", key, func);
logging!(
info,
Type::Hotkey,
true,
"Successfully registered hotkey {} -> {}",
key,
func
);
}
}
_ => {
let key = key.unwrap_or("None");
let func = func.unwrap_or("None");
println!("Invalid hotkey configuration: `{key}`:`{func}`");
log::error!(target: "app", "Invalid hotkey configuration: `{key}`:`{func}`");
logging!(
error,
Type::Hotkey,
true,
"Invalid hotkey configuration: `{}`:`{}`",
key,
func
);
}
}
}
self.current.lock().clone_from(hotkeys);
} else {
println!("No hotkeys configured");
log::info!(target: "app", "No hotkeys configured");
logging!(info, Type::Hotkey, true, "No hotkeys configured");
}
Ok(())
@@ -88,45 +120,60 @@ impl Hotkey {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
println!(
logging!(
info,
Type::Hotkey,
true,
"Attempting to register hotkey: {} for function: {}",
hotkey, func
hotkey,
func
);
log::info!(target: "app", "Attempting to register hotkey: {} for function: {}", hotkey, func);
if manager.is_registered(hotkey) {
println!(
logging!(
info,
Type::Hotkey,
true,
"Hotkey {} was already registered, unregistering first",
hotkey
);
log::info!(target: "app", "Hotkey {} was already registered, unregistering first", hotkey);
manager.unregister(hotkey)?;
}
let f = match func.trim() {
"open_or_close_dashboard" => {
println!("Registering open_or_close_dashboard function");
log::info!(target: "app", "Registering open_or_close_dashboard function");
logging!(
info,
Type::Hotkey,
true,
"Registering open_or_close_dashboard function"
);
|| {
println!("=== Hotkey Dashboard Window Operation Start ===");
log::info!(target: "app", "=== Hotkey Dashboard Window Operation Start ===");
logging!(
info,
Type::Hotkey,
true,
"=== Hotkey Dashboard Window Operation Start ==="
);
// 使用 spawn_blocking 来确保在正确的线程上执行
async_runtime::spawn_blocking(|| {
println!("Toggle dashboard window visibility");
log::info!(target: "app", "Toggle dashboard window visibility");
logging!(
info,
Type::Hotkey,
true,
"Toggle dashboard window visibility"
);
// 检查窗口是否存在
if let Some(window) = handle::Handle::global().get_window() {
// 如果窗口可见,则隐藏它
if window.is_visible().unwrap_or(false) {
println!("Window is visible, hiding it");
log::info!(target: "app", "Window is visible, hiding it");
logging!(info, Type::Window, true, "Window is visible, hiding it");
let _ = window.hide();
} else {
// 如果窗口不可见,则显示它
println!("Window is hidden, showing it");
log::info!(target: "app", "Window is hidden, showing it");
logging!(info, Type::Window, true, "Window is hidden, showing it");
if window.is_minimized().unwrap_or(false) {
let _ = window.unminimize();
}
@@ -135,14 +182,22 @@ impl Hotkey {
}
} else {
// 如果窗口不存在,创建一个新窗口
println!("Window does not exist, creating a new one");
log::info!(target: "app", "Window does not exist, creating a new one");
logging!(
info,
Type::Window,
true,
"Window does not exist, creating a new one"
);
resolve::create_window();
}
});
println!("=== Hotkey Dashboard Window Operation End ===");
log::info!(target: "app", "=== Hotkey Dashboard Window Operation End ===");
logging!(
info,
Type::Hotkey,
true,
"=== Hotkey Dashboard Window Operation End ==="
);
}
}
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
@@ -156,8 +211,7 @@ impl Hotkey {
"hide" => || feat::hide(),
_ => {
println!("Invalid function: {}", func);
log::error!(target: "app", "Invalid function: {}", func);
logging!(error, Type::Hotkey, true, "Invalid function: {}", func);
bail!("invalid function \"{func}\"");
}
};
@@ -166,21 +220,18 @@ impl Hotkey {
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
if event.state == ShortcutState::Pressed {
println!("Hotkey pressed: {:?}", hotkey);
log::info!(target: "app", "Hotkey pressed: {:?}", hotkey);
logging!(info, Type::Hotkey, true, "Hotkey pressed: {:?}", hotkey);
if hotkey.key == Code::KeyQ && is_quit {
if let Some(window) = app_handle.get_webview_window("main") {
if window.is_focused().unwrap_or(false) {
println!("Executing quit function");
log::info!(target: "app", "Executing quit function");
logging!(info, Type::Hotkey, true, "Executing quit function");
f();
}
}
} else {
// 直接执行函数,不做任何状态检查
println!("Executing function directly");
log::info!(target: "app", "Executing function directly");
logging!(info, Type::Hotkey, true, "Executing function directly");
// 获取全局热键状态
let is_enable_global_hotkey = Config::verge()
@@ -203,8 +254,14 @@ impl Hotkey {
}
});
println!("Successfully registered hotkey {} for {}", hotkey, func);
log::info!(target: "app", "Successfully registered hotkey {} for {}", hotkey, func);
logging!(
info,
Type::Hotkey,
true,
"Successfully registered hotkey {} for {}",
hotkey,
func
);
Ok(())
}
@@ -212,7 +269,7 @@ impl Hotkey {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
manager.unregister(hotkey)?;
log::debug!(target: "app", "unregister hotkey {hotkey}");
logging!(debug, Type::Hotkey, true, "Unregister hotkey {}", hotkey);
Ok(())
}
@@ -285,7 +342,13 @@ impl Drop for Hotkey {
fn drop(&mut self) {
let app_handle = handle::Handle::global().app_handle().unwrap();
if let Err(e) = app_handle.global_shortcut().unregister_all() {
log::error!(target:"app", "Error unregistering all hotkeys: {:?}", e);
logging!(
error,
Type::Hotkey,
true,
"Error unregistering all hotkeys: {:?}",
e
);
}
}
}
+72 -37
View File
@@ -1,7 +1,17 @@
use crate::{config::Config, utils::dirs};
use crate::{
config::Config,
logging,
utils::{dirs, logging::Type},
};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, env::current_exe, path::PathBuf, process::Command as StdCommand, time::{SystemTime, UNIX_EPOCH}};
use std::{
collections::HashMap,
env::current_exe,
path::PathBuf,
process::Command as StdCommand,
time::{SystemTime, UNIX_EPOCH},
};
use tokio::time::Duration;
// Windows only
@@ -11,15 +21,15 @@ const REQUIRED_SERVICE_VERSION: &str = "1.0.5"; // 定义所需的服务版本
// 限制重装时间和次数的常量
const REINSTALL_COOLDOWN_SECS: u64 = 300; // 5分钟冷却期
const MAX_REINSTALLS_PER_DAY: u32 = 3; // 每24小时最多重装3次
const ONE_DAY_SECS: u64 = 86400; // 24小时的秒数
const MAX_REINSTALLS_PER_DAY: u32 = 3; // 每24小时最多重装3次
const ONE_DAY_SECS: u64 = 86400; // 24小时的秒数
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct ServiceState {
pub last_install_time: u64, // 上次安装时间戳 (Unix 时间戳,秒)
pub install_count: u32, // 24小时内安装次数
pub last_check_time: u64, // 上次检查时间
pub last_error: Option<String>, // 上次错误信息
pub last_install_time: u64, // 上次安装时间戳 (Unix 时间戳,秒)
pub install_count: u32, // 24小时内安装次数
pub last_check_time: u64, // 上次检查时间
pub last_error: Option<String>, // 上次错误信息
}
impl ServiceState {
@@ -47,12 +57,12 @@ impl ServiceState {
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
// 检查是否需要重置计数器(24小时已过)
if now - self.last_install_time > ONE_DAY_SECS {
self.install_count = 0;
}
self.last_install_time = now;
self.install_count += 1;
}
@@ -63,17 +73,19 @@ impl ServiceState {
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
// 如果在冷却期内,不允许重装
if now - self.last_install_time < REINSTALL_COOLDOWN_SECS {
return false;
}
// 如果24小时内安装次数过多,也不允许
if now - self.last_install_time < ONE_DAY_SECS && self.install_count >= MAX_REINSTALLS_PER_DAY {
if now - self.last_install_time < ONE_DAY_SECS
&& self.install_count >= MAX_REINSTALLS_PER_DAY
{
return false;
}
true
}
}
@@ -112,7 +124,7 @@ pub async fn reinstall_service() -> Result<()> {
// 获取当前服务状态
let mut service_state = ServiceState::get();
// 检查是否允许重装
if !service_state.can_reinstall() {
log::warn!(target:"app", "service reinstall rejected: cooldown period or max attempts reached");
@@ -216,6 +228,12 @@ pub async fn reinstall_service() -> Result<()> {
);
}
// 记录安装信息并保存
let mut service_state = ServiceState::get();
service_state.record_install();
service_state.last_error = None;
service_state.save()?;
Ok(())
}
@@ -268,6 +286,13 @@ pub async fn reinstall_service() -> Result<()> {
status.code().unwrap()
);
}
// 记录安装信息并保存
let mut service_state = ServiceState::get();
service_state.record_install();
service_state.last_error = None;
service_state.save()?;
Ok(())
}
@@ -314,7 +339,7 @@ pub async fn check_service_version() -> Result<String> {
pub async fn check_service_needs_reinstall() -> bool {
// 获取当前服务状态
let service_state = ServiceState::get();
// 首先检查是否在冷却期或超过重装次数限制
if !service_state.can_reinstall() {
log::info!(target: "app", "service reinstall check: in cooldown period or max attempts reached");
@@ -326,21 +351,21 @@ pub async fn check_service_needs_reinstall() -> bool {
Ok(version) => {
// 打印更详细的日志,方便排查问题
log::info!(target: "app", "服务版本检测:当前={}, 要求={}", version, REQUIRED_SERVICE_VERSION);
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
if needs_reinstall {
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={}, 要求={}",
version, REQUIRED_SERVICE_VERSION);
// 打印版本字符串的原始字节,确认没有隐藏字符
log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
} else {
log::info!(target: "app", "服务版本匹配,无需重装");
}
needs_reinstall
},
}
Err(err) => {
// 检查服务是否可用,如果可用但版本检查失败,可能只是版本API有问题
match is_service_running().await {
@@ -408,9 +433,9 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
// 先检查服务版本,不受冷却期限制
let version_check = match check_service_version().await {
Ok(version) => {
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
log::info!(target: "app", "检测到服务版本: {}, 要求版本: {}",
version, REQUIRED_SERVICE_VERSION);
// 通过字节比较确保完全匹配
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
log::warn!(target: "app", "服务版本不匹配,需要重装");
@@ -419,7 +444,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
log::info!(target: "app", "服务版本匹配");
true // 版本匹配
}
},
}
Err(err) => {
log::warn!(target: "app", "无法获取服务版本: {}", err);
false // 无法获取版本
@@ -434,11 +459,11 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
return start_with_existing_service(config_file).await;
}
}
// 强制执行版本检查,如果版本不匹配则重装
if !version_check {
log::info!(target: "app", "服务版本不匹配,尝试重装");
// 获取服务状态,检查是否可以重装
let service_state = ServiceState::get();
if !service_state.can_reinstall() {
@@ -451,22 +476,22 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
bail!("服务版本不匹配且无法重装,启动失败");
}
}
// 尝试重装
log::info!(target: "app", "开始重装服务");
if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "服务重装失败: {}", err);
// 尝试使用现有服务
log::info!(target: "app", "尝试使用现有服务");
return start_with_existing_service(config_file).await;
}
// 重装成功,尝试启动
log::info!(target: "app", "服务重装成功,尝试启动");
return start_with_existing_service(config_file).await;
}
// 检查服务状态
match check_service().await {
Ok(_) => {
@@ -475,22 +500,22 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
if let Ok(()) = start_with_existing_service(config_file).await {
return Ok(());
}
},
}
Err(err) => {
log::warn!(target: "app", "服务检查失败: {}", err);
}
}
// 服务不可用或启动失败,检查是否需要重装
if check_service_needs_reinstall().await {
log::info!(target: "app", "服务需要重装");
// 尝试重装
if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "服务重装失败: {}", err);
bail!("Failed to reinstall service: {}", err);
}
// 重装后再次尝试启动
log::info!(target: "app", "服务重装完成,尝试启动核心");
start_with_existing_service(config_file).await
@@ -521,12 +546,22 @@ pub async fn is_service_running() -> Result<bool> {
// 检查服务状态码和消息
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
logging!(debug, Type::Service, "Service is running");
Ok(true)
} else {
logging!(debug, Type::Service, "Service is not running");
Ok(false)
}
}
pub async fn is_service_available() -> Result<()> {
let resp = check_service().await?;
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
logging!(debug, Type::Service, "Service is available");
}
Ok(())
}
/// 强制重装服务(用于UI中的修复服务按钮)
pub async fn force_reinstall_service() -> Result<()> {
log::info!(target: "app", "用户请求强制重装服务");
@@ -534,15 +569,15 @@ pub async fn force_reinstall_service() -> Result<()> {
// 创建默认服务状态(重置所有限制)
let service_state = ServiceState::default();
service_state.save()?;
log::info!(target: "app", "已重置服务状态,开始执行重装");
// 执行重装
match reinstall_service().await {
Ok(()) => {
log::info!(target: "app", "服务重装成功");
Ok(())
},
}
Err(err) => {
log::error!(target: "app", "强制重装服务失败: {}", err);
bail!("强制重装服务失败: {}", err)
@@ -47,8 +47,6 @@ 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(_) => {
// 更新订阅
@@ -67,7 +65,6 @@ 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();
@@ -37,7 +37,7 @@ impl PlatformSpecification {
tokio::runtime::Handle::current().block_on(async {
match CoreManager::global().get_running_mode().await {
crate::core::RunningMode::Service => "Service".to_string(),
crate::core::RunningMode::Sidecar => "Sidecar".to_string(),
crate::core::RunningMode::Sidecar => "Standalone".to_string(),
crate::core::RunningMode::NotRunning => "Not Running".to_string(),
}
})
@@ -136,52 +136,6 @@ pub fn linux_elevator() -> String {
}
}
#[macro_export]
macro_rules! error {
($result: expr) => {
log::error!(target: "app", "{}", $result);
};
}
#[macro_export]
macro_rules! log_err {
($result: expr) => {
if let Err(err) = $result {
log::error!(target: "app", "{err}");
}
};
($result: expr, $err_str: expr) => {
if let Err(_) = $result {
log::error!(target: "app", "{}", $err_str);
}
};
}
#[macro_export]
macro_rules! trace_err {
($result: expr, $err_str: expr) => {
if let Err(err) = $result {
log::trace!(target: "app", "{}, err {}", $err_str, err);
}
}
}
/// wrap the anyhow error
/// transform the error to String
#[macro_export]
macro_rules! wrap_err {
($stat: expr) => {
match $stat {
Ok(a) => Ok(a),
Err(err) => {
log::error!(target: "app", "{}", err.to_string());
Err(format!("{}", err.to_string()))
}
}
};
}
/// return the string literal error
#[macro_export]
macro_rules! ret_err {
@@ -0,0 +1,116 @@
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Type {
Core,
Service,
Hotkey,
Window,
}
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Type::Core => write!(f, "[Core]"),
Type::Service => write!(f, "[Service]"),
Type::Hotkey => write!(f, "[Hotkey]"),
Type::Window => write!(f, "[Window]"),
}
}
}
#[macro_export]
macro_rules! error {
($result: expr) => {
log::error!(target: "app", "{}", $result);
};
}
#[macro_export]
macro_rules! log_err {
($result: expr) => {
if let Err(err) = $result {
log::error!(target: "app", "{err}");
}
};
($result: expr, $err_str: expr) => {
if let Err(_) = $result {
log::error!(target: "app", "{}", $err_str);
}
};
}
#[macro_export]
macro_rules! trace_err {
($result: expr, $err_str: expr) => {
if let Err(err) = $result {
log::trace!(target: "app", "{}, err {}", $err_str, err);
}
}
}
/// wrap the anyhow error
/// transform the error to String
#[macro_export]
macro_rules! wrap_err {
($stat: expr) => {
match $stat {
Ok(a) => Ok(a),
Err(err) => {
log::error!(target: "app", "{}", err.to_string());
Err(format!("{}", err.to_string()))
}
}
};
}
#[macro_export]
macro_rules! logging {
// 带 println 的版本(支持格式化参数)
($level:ident, $type:expr, $print:expr, $($arg:tt)*) => {
println!("{} {}", $type, format_args!($($arg)*));
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
};
// 不带 println 的版本
($level:ident, $type:expr, $($arg:tt)*) => {
log::$level!(target: "app", "{} {}", $type, format_args!($($arg)*));
};
}
#[macro_export]
macro_rules! logging_error {
// Version with println and Result expression
($type:expr, $print:expr, $expr:expr) => {
match $expr {
Ok(_) => {},
Err(err) => {
if $print {
println!("[{}] Error: {}", $type, err);
}
log::error!(target: "app", "{} {}", $type, err);
}
}
};
// Version without println and Result expression
($type:expr, $expr:expr) => {
if let Err(err) = $expr {
log::error!(target: "app", "{} {}", $type, err);
}
};
// Version with println and custom message
($type:expr, $print:expr, $($arg:tt)*) => {
if $print {
println!("[{}] {}", $type, format_args!($($arg)*));
}
log::error!(target: "app", "{} {}", $type, format_args!($($arg)*));
};
// Version without println and custom message
($type:expr, $($arg:tt)*) => {
log::error!(target: "app", "{} {}", $type, format_args!($($arg)*));
};
}
@@ -3,6 +3,7 @@ pub mod error;
pub mod help;
pub mod i18n;
pub mod init;
pub mod logging;
pub mod resolve;
pub mod server;
pub mod tmpl;
+31 -7
View File
@@ -1,5 +1,5 @@
{
"version": "2.2.1",
"version": "2.2.3-alpha",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
@@ -11,9 +11,15 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": ["resources", "resources/locales/*"],
"resources": [
"resources",
"resources/locales/*"
],
"publisher": "Clash Verge Rev",
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"externalBin": [
"sidecar/verge-mihomo",
"sidecar/verge-mihomo-alpha"
],
"copyright": "GNU General Public License v3.0",
"category": "DeveloperTool",
"shortDescription": "Clash Verge Rev",
@@ -30,24 +36,42 @@
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
"endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater-alpha/update-alpha.json"
],
"windows": {
"installMode": "basicUi"
}
},
"deep-link": {
"desktop": {
"schemes": ["clash", "clash-verge"]
"schemes": [
"clash",
"clash-verge"
]
}
}
},
"app": {
"security": {
"capabilities": ["desktop-capability", "migrated"],
"capabilities": [
"desktop-capability",
"migrated"
],
"assetProtocol": {
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"],
"scope": [
"$APPDATA/**",
"$RESOURCE/../**",
"**"
],
"enable": true
},
"csp": null
}
}
}
}
-1
View File
@@ -353,7 +353,6 @@
"clash_mode_direct": "直连模式",
"toggle_system_proxy": "打开/关闭系统代理",
"toggle_tun_mode": "打开/关闭 TUN 模式",
"toggle_lightweight_mode": "进入轻量模式",
"entry_lightweight_mode": "进入轻量模式",
"Backup Setting": "备份设置",
"Backup Setting Info": "支持 WebDAV 备份配置文件",
+2 -2
View File
@@ -249,8 +249,8 @@ const Layout = () => {
? {
borderRadius: "8px",
border: "1px solid var(--divider-color)",
width: "calc(100vw - 0px)",
height: "calc(100vh - 0px)",
width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)",
}
: {},
]}
+4 -4
View File
@@ -2054,9 +2054,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.26"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
dependencies = [
"serde",
]
@@ -3388,7 +3388,7 @@ dependencies = [
[[package]]
name = "shadowsocks-rust"
version = "1.23.0"
version = "1.23.1"
dependencies = [
"base64",
"build-time",
@@ -3429,7 +3429,7 @@ dependencies = [
[[package]]
name = "shadowsocks-service"
version = "1.23.0"
version = "1.23.1"
dependencies = [
"arc-swap",
"brotli",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "shadowsocks-rust"
version = "1.23.0"
version = "1.23.1"
authors = ["Shadowsocks Contributors"]
description = "shadowsocks is a fast tunnel proxy that helps you bypass firewalls."
repository = "https://github.com/shadowsocks/shadowsocks-rust"
@@ -242,7 +242,7 @@ jemallocator = { version = "0.5", optional = true }
snmalloc-rs = { version = "0.3", optional = true }
rpmalloc = { version = "0.2", optional = true }
shadowsocks-service = { version = "1.23.0", path = "./crates/shadowsocks-service" }
shadowsocks-service = { version = "1.23.1", path = "./crates/shadowsocks-service" }
windows-service = { version = "0.8", optional = true }
@@ -1,6 +1,6 @@
[package]
name = "shadowsocks-service"
version = "1.23.0"
version = "1.23.1"
authors = ["Shadowsocks Contributors"]
description = "shadowsocks is a fast tunnel proxy that helps you bypass firewalls."
repository = "https://github.com/shadowsocks/shadowsocks-rust"
@@ -185,6 +185,7 @@ smoltcp = { version = "0.12", optional = true, default-features = false, feature
"log",
"medium-ip",
"proto-ipv4",
"proto-ipv4-fragmentation",
"proto-ipv6",
"socket-icmp",
"socket-udp",
@@ -18,7 +18,7 @@ use log::{debug, error, trace};
use shadowsocks::{net::TcpSocketOpts, relay::socks5::Address};
use smoltcp::{
iface::{Config as InterfaceConfig, Interface, PollResult, SocketHandle, SocketSet},
phy::{DeviceCapabilities, Medium},
phy::{Checksum, DeviceCapabilities, Medium},
socket::tcp::{CongestionControl, Socket as TcpSocket, SocketBuffer as TcpSocketBuffer, State as TcpState},
storage::RingBuffer,
time::{Duration as SmolDuration, Instant as SmolInstant},
@@ -42,9 +42,9 @@ use crate::{
use super::virt_device::VirtTunDevice;
// NOTE: Default buffer could contain 20 AEAD packets
const DEFAULT_TCP_SEND_BUFFER_SIZE: u32 = 0x3FFF * 20;
const DEFAULT_TCP_RECV_BUFFER_SIZE: u32 = 0x3FFF * 20;
// NOTE: Default buffer could contain 5 AEAD packets
const DEFAULT_TCP_SEND_BUFFER_SIZE: u32 = (0x3FFFu32 * 5).next_power_of_two();
const DEFAULT_TCP_RECV_BUFFER_SIZE: u32 = (0x3FFFu32 * 5).next_power_of_two();
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum TcpSocketState {
@@ -261,6 +261,11 @@ impl TcpTun {
let mut capabilities = DeviceCapabilities::default();
capabilities.medium = Medium::Ip;
capabilities.max_transmission_unit = mtu as usize;
capabilities.checksum.ipv4 = Checksum::Tx;
capabilities.checksum.tcp = Checksum::Tx;
capabilities.checksum.udp = Checksum::Tx;
capabilities.checksum.icmpv4 = Checksum::Tx;
capabilities.checksum.icmpv6 = Checksum::Tx;
let (mut device, iface_rx, iface_tx, iface_tx_avail) = VirtTunDevice::new(capabilities);
@@ -64,6 +64,7 @@ impl Device for VirtTunDevice {
buffer,
phantom_device: PhantomData,
};
self.in_buf_avail.store(true, Ordering::Release);
let tx = VirtTxToken(self);
return Some((rx, tx));
}
+9 -9
View File
@@ -55,7 +55,7 @@ jobs:
- name: Calculate version
if: github.event_name != 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/read_tag --nightly
go run -v ./cmd/internal/read_tag --ci --nightly
- name: Set outputs
id: outputs
run: |-
@@ -229,8 +229,8 @@ jobs:
cd dist
mkdir -p "${DIR_NAME}"
cp ../LICENSE "${DIR_NAME}"
if [ '${{ matrix.os }}' = 'windoes' ]; then
cp sing-box.exe "${DIR_NAME}"
if [ '${{ matrix.os }}' = 'windows' ]; then
cp sing-box "${DIR_NAME}/sing-box.exe"
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
else
cp sing-box "${DIR_NAME}"
@@ -316,9 +316,9 @@ jobs:
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
- name: Prepare upload
run: |-
mkdir -p dist/release
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
mkdir -p dist
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
@@ -566,9 +566,9 @@ jobs:
zip -r SFM.dSYMs.zip dSYMs
popd
mkdir -p dist/release
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
mkdir -p dist
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
- name: Upload image
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
+4 -4
View File
@@ -34,7 +34,7 @@ jobs:
- name: Calculate version
if: github.event_name != 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/read_tag --nightly
go run -v ./cmd/internal/read_tag --ci --nightly
- name: Set outputs
id: outputs
run: |-
@@ -116,6 +116,7 @@ jobs:
sudo gem install fpm
sudo apt-get install -y debsigs
fpm -t deb \
--name "${NAME}" \
-v "${{ needs.calculate_version.outputs.version }}" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
--architecture ${{ matrix.debian }} \
@@ -133,6 +134,7 @@ jobs:
set -xeuo pipefail
sudo gem install fpm
fpm -t rpm \
--name "${NAME}" \
-v "${{ needs.calculate_version.outputs.version }}" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
--architecture ${{ matrix.rpm }} \
@@ -175,6 +177,4 @@ jobs:
merge-multiple: true
- name: Publish packages
run: |-
wget -O fury-cli.deb https://github.com/gemfury/cli/releases/download/v0.23.0/fury-cli_0.23.0_linux_amd64.deb
sudo dpkg -i fury-cli.deb
fury migrate dist --as=sagernet --api-token ${{ secrets.FURY_TOKEN }}
ls dist | xargs -I {} curl -F "package=@dist/{}" -u "${{ secrets.FURY_TOKEN }}" -X POST https://push.fury.io/sagernet/
+25 -13
View File
@@ -5,40 +5,52 @@ import (
"os"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing-box/log"
)
var nightly bool
var (
flagRunInCI bool
flagRunNightly bool
)
func init() {
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
}
func main() {
flag.Parse()
if nightly {
version, err := build_shared.ReadTagVersionRev()
var (
versionStr string
err error
)
if flagRunNightly {
var version badversion.Version
version, err = build_shared.ReadTagVersionRev()
if err == nil {
if version.PreReleaseIdentifier == "" {
version.Patch++
}
versionStr = version.String()
}
} else {
versionStr, err = build_shared.ReadTag()
}
if flagRunInCI {
if err != nil {
log.Fatal(err)
}
var versionStr string
if version.PreReleaseIdentifier != "" {
versionStr = version.VersionString() + "-nightly"
} else {
version.Patch++
versionStr = version.VersionString() + "-nightly"
}
err = setGitHubEnv("version", versionStr)
if err != nil {
log.Fatal(err)
}
} else {
tag, err := build_shared.ReadTag()
if err != nil {
log.Error(err)
os.Stdout.WriteString("unknown\n")
} else {
os.Stdout.WriteString(tag + "\n")
os.Stdout.WriteString(versionStr + "\n")
}
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ require (
github.com/sagernet/cors v1.2.1
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.4
github.com/sagernet/gvisor v0.0.0-20250324121324-d3f3d7570296
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.49.0-beta.1
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.6.5-0.20250324102321-1ddf4ccbfab8
+2 -2
View File
@@ -167,8 +167,8 @@ github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQ
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/gomobile v0.1.4 h1:WzX9ka+iHdupMgy2Vdich+OAt7TM8C2cZbIbzNjBrJY=
github.com/sagernet/gomobile v0.1.4/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E=
github.com/sagernet/gvisor v0.0.0-20250324121324-d3f3d7570296 h1:zovOW85AOnNgGS5dzR/M5Ly1qTRt3a8Ymiu0TIonvAE=
github.com/sagernet/gvisor v0.0.0-20250324121324-d3f3d7570296/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4=
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38=
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=189
PKG_RELEASE:=6
PKG_RELEASE:=7
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY \
@@ -166,7 +166,7 @@ if is_finded("naive") then
o:value("naiveproxy", translate("NaiveProxy"))
end
if is_finded("hysteria") then
o:value("hysteria", translate("Hysteria"))
o:value("hysteria2", translate("Hysteria2"))
end
if is_finded("tuic-client") then
o:value("tuic", translate("TUIC"))
@@ -254,7 +254,7 @@ o:depends("type", "ss")
o:depends("type", "v2ray")
o:depends("type", "trojan")
o:depends("type", "naiveproxy")
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o:depends("type", "tuic")
o:depends("type", "shadowtls")
o:depends("type", "socks5")
@@ -267,7 +267,7 @@ o:depends("type", "ss")
o:depends("type", "v2ray")
o:depends("type", "trojan")
o:depends("type", "naiveproxy")
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o:depends("type", "tuic")
o:depends("type", "shadowtls")
o:depends("type", "socks5")
@@ -383,100 +383,100 @@ o:depends("type", "ssr")
-- [[ Hysteria2 ]]--
o = s:option(Value, "hy2_auth", translate("Users Authentication"))
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o.rmempty = false
o = s:option(Flag, "flag_port_hopping", translate("Enable Port Hopping"))
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o.rmempty = true
o.default = "0"
o = s:option(Value, "port_range", translate("Port Range"))
o:depends({type = "hysteria", flag_port_hopping = true})
o:depends({type = "hysteria2", flag_port_hopping = true})
o.datatype = "portrange"
o.rmempty = true
o = s:option(Flag, "flag_transport", translate("Enable Transport Protocol Settings"))
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o.rmempty = true
o.default = "0"
o = s:option(ListValue, "transport_protocol", translate("Transport Protocol"))
o:depends({type = "hysteria", flag_transport = true})
o:depends({type = "hysteria2", flag_transport = true})
o:value("udp", translate("UDP"))
o.default = "udp"
o.rmempty = true
o = s:option(Value, "hopinterval", translate("Port Hopping Interval(Unit:Second)"))
o:depends({type = "hysteria", flag_transport = true, flag_port_hopping = true})
o:depends({type = "hysteria2", flag_transport = true, flag_port_hopping = true})
o.datatype = "uinteger"
o.rmempty = true
o.default = "30"
o = s:option(Flag, "flag_obfs", translate("Enable Obfuscation"))
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o.rmempty = true
o.default = "0"
o = s:option(Flag, "lazy_mode", translate("Enable Lazy Mode"))
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o.rmempty = true
o.default = "0"
o = s:option(Value, "obfs_type", translate("Obfuscation Type"))
o:depends({type = "hysteria", flag_obfs = "1"})
o:depends({type = "hysteria2", flag_obfs = "1"})
o.rmempty = true
o.default = "salamander"
o = s:option(Value, "salamander", translate("Obfuscation Password"))
o:depends({type = "hysteria", flag_obfs = "1"})
o:depends({type = "hysteria2", flag_obfs = "1"})
o.rmempty = true
o.default = "cry_me_a_r1ver"
o = s:option(Flag, "flag_quicparam", translate("Hysterir QUIC parameters"))
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o.rmempty = true
o.default = "0"
o = s:option(Flag, "disablepathmtudiscovery", translate("Disable QUIC path MTU discovery"))
o:depends({type = "hysteria",flag_quicparam = "1"})
o:depends({type = "hysteria2",flag_quicparam = "1"})
o.rmempty = true
o.default = false
--[[Hysteria2 QUIC parameters setting]]
o = s:option(Value, "initstreamreceivewindow", translate("QUIC initStreamReceiveWindow"))
o:depends({type = "hysteria", flag_quicparam = "1"})
o:depends({type = "hysteria2", flag_quicparam = "1"})
o.datatype = "uinteger"
o.rmempty = true
o.default = "8388608"
o = s:option(Value, "maxstreamseceivewindow", translate("QUIC maxStreamReceiveWindow"))
o:depends({type = "hysteria", flag_quicparam = "1"})
o:depends({type = "hysteria2", flag_quicparam = "1"})
o.datatype = "uinteger"
o.rmempty = true
o.default = "8388608"
o = s:option(Value, "initconnreceivewindow", translate("QUIC initConnReceiveWindow"))
o:depends({type = "hysteria", flag_quicparam = "1"})
o:depends({type = "hysteria2", flag_quicparam = "1"})
o.datatype = "uinteger"
o.rmempty = true
o.default = "20971520"
o = s:option(Value, "maxconnreceivewindow", translate("QUIC maxConnReceiveWindow"))
o:depends({type = "hysteria", flag_quicparam = "1"})
o:depends({type = "hysteria2", flag_quicparam = "1"})
o.datatype = "uinteger"
o.rmempty = true
o.default = "20971520"
o = s:option(Value, "maxidletimeout", translate("QUIC maxIdleTimeout(Unit:second)"))
o:depends({type = "hysteria", flag_quicparam = "1"})
o:depends({type = "hysteria2", flag_quicparam = "1"})
o.rmempty = true
o.datatype = "uinteger"
o.default = "30"
o = s:option(Value, "keepaliveperiod", translate("The keep-alive period.(Unit:second)"))
o.description = translate("Default value 0 indicatesno heartbeat.")
o:depends({type = "hysteria", flag_quicparam = "1"})
o:depends({type = "hysteria2", flag_quicparam = "1"})
o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
o.rmempty = true
o.datatype = "uinteger"
@@ -950,14 +950,14 @@ o.rmempty = true
o = s:option(Value, "uplink_capacity", translate("Uplink Capacity(Default:Mbps)"))
o.datatype = "uinteger"
o:depends("transport", "kcp")
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o.default = 5
o.rmempty = true
o = s:option(Value, "downlink_capacity", translate("Downlink Capacity(Default:Mbps)"))
o.datatype = "uinteger"
o:depends("transport", "kcp")
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o.default = 20
o.rmempty = true
@@ -1030,7 +1030,7 @@ o:depends({type = "v2ray", v2ray_protocol = "shadowsocks", reality = false})
o:depends({type = "v2ray", v2ray_protocol = "socks", socks_ver = "5", reality = false})
o:depends({type = "v2ray", v2ray_protocol = "http", reality = false})
o:depends("type", "trojan")
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
-- [[ TLS部分 ]] --
o = s:option(Flag, "tls_sessionTicket", translate("Session Ticket"))
@@ -1117,12 +1117,12 @@ o.rmempty = true
o = s:option(Flag, "insecure", translate("allowInsecure"))
o.rmempty = false
o:depends("tls", true)
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o.description = translate("If true, allowss insecure connection at TLS client, e.g., TLS server uses unverifiable certificates.")
-- [[ Hysteria2 TLS pinSHA256 ]] --
o = s:option(Value, "pinsha256", translate("Certificate fingerprint"))
o:depends({type = "hysteria", insecure = true })
o:depends({type = "hysteria2", insecure = true })
o.rmempty = true
@@ -1230,7 +1230,7 @@ o = s:option(Flag, "certificate", translate("Self-signed Certificate"))
o.rmempty = true
o.default = "0"
o:depends("type", "tuic")
o:depends({type = "hysteria", insecure = false})
o:depends({type = "hysteria2", insecure = false})
o:depends({type = "trojan", tls = true, insecure = false})
o:depends({type = "v2ray", v2ray_protocol = "vmess", tls = true, insecure = false})
o:depends({type = "v2ray", v2ray_protocol = "vless", tls = true, insecure = false})
@@ -1272,9 +1272,9 @@ end
o = s:option(Value, "certpath", translate("Current Certificate Path"))
o:depends("certificate", 1)
o:value("/etc/ssl/private/ca.pem")
o:value("/etc/ssl/private/ca.crt")
o.description = translate("Please confirm the current certificate path")
o.default = "/etc/ssl/private/ca.pem"
o.default = "/etc/ssl/private/ca.crt"
o = s:option(Flag, "fast_open", translate("TCP Fast Open"), translate("Enabling TCP Fast Open Requires Server Support."))
o.rmempty = true
@@ -1282,7 +1282,7 @@ o.default = "0"
o:depends("type", "ssr")
o:depends("type", "ss")
o:depends("type", "trojan")
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o = s:option(Flag, "switch_enable", translate("Enable Auto Switch"))
o.rmempty = false
@@ -1318,3 +1318,5 @@ if is_finded("kcptun-client") then
end
return m
@@ -99,7 +99,7 @@ function import_ssr_url(btn, urlname, sid) {
var event = document.createEvent("HTMLEvents");
event.initEvent("change", true, true);
switch (ssu[0]) {
case "hysteria":
case "hysteria2":
try {
var url = new URL("http://" + ssu[1]);
var params = url.searchParams;
@@ -112,16 +112,40 @@ function import_ssr_url(btn, urlname, sid) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port || "80";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hysteria_protocol')[0].value = params.get("protocol") || "udp";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.auth_type')[0].value = params.get("auth") ? "2" : "0";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.auth_type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.auth_payload')[0].value = params.get("auth") || "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.uplink_capacity')[0].value = params.get("upmbps") || "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.downlink_capacity')[0].value = params.get("downmbps") || "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.seed')[0].value = params.get("obfsParam") || "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = params.get("peer") || "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.quic_tls_alpn')[0].value = params.get("alpn") || "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.insecure')[0].checked = params.get("insecure") ? true : false;
if (params.get("lazy") === "1") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].checked = true;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].dispatchEvent(event);
}
if (params.get("protocol") && params.get("protocol") !== undefined) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_transport')[0].checked = true; // 设置 flag_transport 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_transport')[0].dispatchEvent(event); // 触发事件
document.getElementsByName('cbid.shadowsocksr.' + sid + '.transport_protocol')[0].value = params.get("protocol") || "udp";
}
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].value = decodeURIComponent(url.username);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.uplink_capacity')[0].value = params.get("upmbps") || "5";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.downlink_capacity')[0].value = params.get("downmbps") || "20";
if (params.get("obfs") && (params.get("obfs") !== undefined)) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_obfs')[0].checked = true; // 设置 flag_obfs 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_obfs')[0].dispatchEvent(event); // 触发事件
document.getElementsByName('cbid.shadowsocksr.' + sid + '.obfs_type')[0].value = params.get("obfs") || "salamander";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.salamander')[0].value = params.get("obfs-password") || "cry_me_a_r1ver";
}
if (params.get("sni")) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; // 设置 flag_obfs 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event); // 触发事件
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = params.get("sni") || "";
}
if (params.get("insecure") === "1") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.insecure')[0].checked = true;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.insecure')[0].dispatchEvent(event);
if (params.get("sni")) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.pinsha256')[0].value = params.get("pinsha256") || "";
}
}
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
@@ -550,3 +574,4 @@ function import_ssr_url(btn, urlname, sid) {
<span id="<%=self.option%>-status"></span>
<%+cbi/valuefooter%>
@@ -391,7 +391,7 @@ gen_config_file() { #server1 type2 code3 local_port4 socks_port5 chain6 threads5
;;
esac
;;
hysteria)
hysteria2)
lua /usr/share/shadowsocksr/gen_config.lua $1 $mode $4 $5 >$config_file
;;
tuic)
@@ -468,7 +468,7 @@ start_udp() {
redir_udp=0
ARG_UDP=""
;;
hysteria)
hysteria2)
gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_port
ln_start_bin $(first_type hysteria) hysteria client --config $udp_config_file
echolog "UDP TPROXY Relay:$($(first_type "hysteria") version | grep Version | awk '{print "Hysteria2: " $2}') Started!"
@@ -648,7 +648,7 @@ start_shunt() {
echolog "shunt:$($(first_type "naive") --version 2>&1 | head -1) Started!"
redir_udp=0
;;
hysteria)
hysteria2)
if [ -n "$tmp_local_port" ]; then
local tmp_port=$tmp_local_port
gen_config_file $SHUNT_SERVER $type 3 $tmp_shunt_port
@@ -765,7 +765,7 @@ start_local() {
ln_start_bin $(first_type naive) naive --config $local_config_file
echolog "Global_Socks5:$($(first_type naive) --version | head -1) Started!"
;;
hysteria)
hysteria2)
if [ "$_local" == "2" ]; then
gen_config_file $LOCAL_SERVER $type 4 0 $local_port
ln_start_bin $(first_type hysteria) hysteria client --config $local_config_file
@@ -873,7 +873,7 @@ Start_Run() {
ln_start_bin $(first_type naive) naive $tcp_config_file
echolog "Main node:$($(first_type naive) --version 2>&1 | head -1) , $threads Threads Started!"
;;
hysteria)
hysteria2)
gen_config_file $GLOBAL_SERVER $type 1 $tcp_port $socks_port
ln_start_bin $(first_type hysteria) hysteria client --config $tcp_config_file
echolog "Main node:$($(first_type hysteria) version | grep Version | awk '{print "Hysteria2: " $2}') Started!"
@@ -411,7 +411,7 @@ local ss = {
fast_open = (server.fast_open == "1") and true or false,
reuse_port = true
}
local hysteria = {
local hysteria2 = {
server = (server.server_port and (server.port_range and (server.server .. ":" .. server.server_port .. "," .. server.port_range) or (server.server .. ":" .. server.server_port) or (server.port_range and server.server .. ":" .. server.port_range or server.server .. ":443"))),
bandwidth = (server.uplink_capacity or server.downlink_capacity) and {
up = tonumber(server.uplink_capacity) and tonumber(server.uplink_capacity) .. " mbps" or nil,
@@ -606,8 +606,8 @@ function config:handleIndex(index)
naiveproxy = function()
print(json.stringify(naiveproxy, 1))
end,
hysteria = function()
print(json.stringify(hysteria, 1))
hysteria2 = function()
print(json.stringify(hysteria2, 1))
end,
shadowtls = function()
local chain_switch = {
@@ -10,6 +10,7 @@ require "luci.util"
require "luci.sys"
require "luci.jsonc"
require "luci.model.ipkg"
local ucursor = require "luci.model.uci".cursor()
-- these global functions are accessed all the time by the event handler
-- so caching them is worth the effort
@@ -41,6 +42,7 @@ end
local v2_ss = luci.sys.exec('type -t -p ' .. ss_program .. ' 2>/dev/null') ~= "" and "ss" or "v2ray"
local has_ss_type = luci.sys.exec('type -t -p ' .. ss_program .. ' 2>/dev/null') ~= "" and ss_type
local v2_tj = luci.sys.exec('type -t -p trojan') ~= "" and "trojan" or "v2ray"
local hy2_type = luci.sys.exec('type -t -p hysteria') ~= "" and "hysteria2"
local log = function(...)
print(os.date("%Y-%m-%d %H:%M:%S ") .. table.concat({...}, " "))
end
@@ -156,7 +158,37 @@ end
-- 处理数据
local function processData(szType, content)
local result = {type = szType, local_port = 1234, kcp_param = '--nocomp'}
if szType == 'ssr' then
if szType == "hysteria2" then
local url = URL.parse("http://" .. content)
local params = url.query
result.alias = url.fragment and UrlDecode(url.fragment) or nil
result.type = hy2_type
result.server = url.host
result.server_port = url.port
if params.protocol then
result.flag_transport = "1"
result.transport_protocol = params.protocol or "udp"
end
result.hy2_auth = url.user
result.uplink_capacity = params.upmbps
result.downlink_capacity = params.downmbps
if params.obfs and params.obfs-password then
result.flag_obfs = "1"
result.transport_protocol = params.obfs
result.transport_protocol = params.obfs-password
end
if params.sni then
result.tls = "1"
result.tls_host = params.sni
end
if params.insecure then
result.insecure = "1"
if params.sni then
result.pinsha256 = params.pinsha256
end
end
elseif szType == 'ssr' then
local dat = split(content, "/%?")
local hostInfo = split(dat[1], ':')
result.type = 'ssr'
@@ -309,6 +341,7 @@ local function processData(szType, content)
result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
else
result.plugin = plugin_info
result.plugin_opts = ""
end
-- 部分机场下发的插件名为 simple-obfs,这里应该改为 obfs-local
if result.plugin == "simple-obfs" then
@@ -568,7 +601,7 @@ local function processData(szType, content)
end
-- wget
local function wget(url)
local stdout = luci.sys.exec('wget -q --user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36" --no-check-certificate -O- "' .. url .. '"')
local stdout = luci.sys.exec('wget-ssl -q --user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36" --no-check-certificate -O- "' .. url .. '"')
return trim(stdout)
end
@@ -782,3 +815,4 @@ if subscribe_url and #subscribe_url > 0 then
end)
end
@@ -32,10 +32,10 @@ import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.system.Os
import android.widget.Toast
import androidx.core.os.bundleOf
import com.v2ray.ang.AngApplication
import com.v2ray.ang.extension.listenForPackageChanges
import com.v2ray.ang.extension.toast
import com.v2ray.ang.plugin.PluginContract.METADATA_KEY_ID
import java.io.File
import java.io.FileNotFoundException
@@ -126,7 +126,7 @@ object PluginManager {
if (providers.size > 1) {
val message =
"Conflicting plugins found from: ${providers.joinToString { it.providerInfo.packageName }}"
Toast.makeText(AngApplication.application, message, Toast.LENGTH_LONG).show()
AngApplication.application.toast(message)
throw IllegalStateException(message)
}
val provider = providers.single().providerInfo
@@ -224,8 +224,8 @@ object PluginManager {
fun ComponentInfo.loadString(key: String) = when (val value = metaData.getString(key)) {
is String -> value
is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
.getString(value)
// is Int -> AngApplication.application.packageManager.getResourcesForApplication(applicationInfo)
// .getString(value)
null -> null
else -> error("meta-data $key has invalid type ${value.javaClass}")
@@ -1,6 +1,5 @@
package com.v2ray.ang.service
import android.annotation.TargetApi
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -9,6 +8,7 @@ import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
@@ -16,7 +16,7 @@ import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils
import java.lang.ref.SoftReference
@TargetApi(Build.VERSION_CODES.N)
@RequiresApi(Build.VERSION_CODES.N)
class QSTileService : TileService() {
/**
@@ -5,7 +5,6 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
@@ -14,6 +13,7 @@ import com.v2ray.ang.AppConfig
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityAboutBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.ZipUtil
@@ -195,8 +195,4 @@ class AboutActivity : BaseActivity() {
}
}
}
private fun toast(messageResId: Int) {
Toast.makeText(this, getString(messageResId), Toast.LENGTH_SHORT).show()
}
}
@@ -78,7 +78,7 @@ class ServerCustomConfigActivity : BaseActivity() {
CustomFmt.parse(binding.editor.text.toString())
} catch (e: Exception) {
e.printStackTrace()
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
toast("${getString(R.string.toast_malformed_josn)} ${e.cause?.message}")
return false
}
@@ -162,7 +162,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
continue
}
if (keywordFilter.isEmpty() || profile.remarks.contains(keywordFilter)) {
if (keywordFilter.isEmpty() || profile.remarks.lowercase().contains(keywordFilter.lowercase())) {
serversCache.add(ServersCache(guid, profile))
}
}
@@ -64,7 +64,7 @@
<LinearLayout
android:id="@+id/layout_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="@dimen/view_height_dp64"
android:clickable="true"
android:focusable="true"
android:nextFocusLeft="@+id/recycler_view"
@@ -79,12 +79,11 @@
<TextView
android:id="@+id/tv_test_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:maxLines="2"
android:minLines="1"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:layout_marginBottom="@dimen/padding_spacing_dp8"
android:padding="@dimen/padding_spacing_dp16"
android:gravity="start|center"
android:paddingStart="@dimen/padding_spacing_dp16"
android:text="@string/connection_test_pending"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
@@ -92,18 +91,18 @@
</LinearLayout>
<FrameLayout
android:id="@+id/fabProgressCircle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/padding_spacing_dp16">
android:layout_marginEnd="@dimen/padding_spacing_dp16"
android:layout_marginBottom="@dimen/padding_spacing_dp8">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/padding_spacing_dp16"
android:layout_marginBottom="@dimen/view_height_dp64"
android:clickable="true"
android:focusable="true"
android:nextFocusLeft="@+id/layout_test"
@@ -133,11 +133,11 @@
<string name="menu_item_select_all">Выбрать все</string>
<string name="msg_enter_keywords">Введите ключевые слова</string>
<string name="switch_bypass_apps_mode">Режим обхода</string>
<string name="menu_item_select_proxy_app">Автовыбор проксируемых приложений</string>
<string name="menu_item_select_proxy_app">Автовыбор приложений</string>
<string name="msg_downloading_content">Загрузка данных</string>
<string name="menu_item_export_proxy_app">Экспорт в буфер обмена</string>
<string name="menu_item_import_proxy_app">Импорт из буфера обмена</string>
<string name="per_app_proxy_settings">Настройки выбранных приложений</string>
<string name="per_app_proxy_settings">Выбор приложений</string>
<string name="per_app_proxy_settings_enable">Использовать выбор приложений</string>
<!-- Preferences -->
@@ -173,7 +173,7 @@
<string name="summary_pref_local_dns_enabled">Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string>
<string name="title_pref_fake_dns_enabled">Использовать поддельную DNS</string>
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельный IP-адрес (быстрее, но может не работать с некоторыми приложениями)</string>
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
<string name="summary_pref_prefer_ipv6">Предпочитать IPv6-адреса и маршрутизацию</string>
@@ -5,5 +5,6 @@
<dimen name="padding_spacing_dp16">16dp</dimen>
<dimen name="image_size_dp24">24dp</dimen>
<dimen name="view_height_dp48">48dp</dimen>
<dimen name="view_height_dp64">64dp</dimen>
<dimen name="view_height_dp160">160dp</dimen>
</resources>
+1
View File
@@ -757,3 +757,4 @@ rysson
somini
thedenv
vallovic
arabcoders
+27
View File
@@ -4,6 +4,33 @@
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
-->
### 2025.03.25
#### Core changes
- [Fix attribute error on failed VT init](https://github.com/yt-dlp/yt-dlp/commit/b872ffec50fd50f790a5a490e006a369a28a3df3) ([#12696](https://github.com/yt-dlp/yt-dlp/issues/12696)) by [Grub4K](https://github.com/Grub4K)
- **utils**: `js_to_json`: [Make function less fatal](https://github.com/yt-dlp/yt-dlp/commit/9491b44032b330e05bd5eaa546187005d1e8538e) ([#12715](https://github.com/yt-dlp/yt-dlp/issues/12715)) by [seproDev](https://github.com/seproDev)
#### Extractor changes
- [Fix sorting of HLS audio formats by `GROUP-ID`](https://github.com/yt-dlp/yt-dlp/commit/86ab79e1a5182092321102adf6ca34195803b878) ([#12714](https://github.com/yt-dlp/yt-dlp/issues/12714)) by [bashonly](https://github.com/bashonly)
- **17live**: vod: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/3396eb50dcd245b49c0f4aecd6e80ec914095d16) ([#12723](https://github.com/yt-dlp/yt-dlp/issues/12723)) by [subrat-lima](https://github.com/subrat-lima)
- **9now.com.au**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/9d5e6de2e7a47226d1f72c713ad45c88ba01db68) ([#12702](https://github.com/yt-dlp/yt-dlp/issues/12702)) by [bashonly](https://github.com/bashonly)
- **chzzk**: video: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/e2dfccaf808b406d5bcb7dd04ae9ce420752dd6f) ([#12692](https://github.com/yt-dlp/yt-dlp/issues/12692)) by [bashonly](https://github.com/bashonly), [dirkf](https://github.com/dirkf)
- **deezer**: [Remove extractors](https://github.com/yt-dlp/yt-dlp/commit/be5af3f9e91747768c2b41157851bfbe14c663f7) ([#12704](https://github.com/yt-dlp/yt-dlp/issues/12704)) by [seproDev](https://github.com/seproDev)
- **generic**: [Fix MPD base URL parsing](https://github.com/yt-dlp/yt-dlp/commit/5086d4aed6aeb3908c62f49e2d8f74cc0cb05110) ([#12718](https://github.com/yt-dlp/yt-dlp/issues/12718)) by [fireattack](https://github.com/fireattack)
- **streaks**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/801afeac91f97dc0b58cd39cc7e8c50f619dc4e1) ([#12679](https://github.com/yt-dlp/yt-dlp/issues/12679)) by [doe1080](https://github.com/doe1080)
- **tver**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/66e0bab814e4a52ef3e12d81123ad992a29df50e) ([#12659](https://github.com/yt-dlp/yt-dlp/issues/12659)) by [arabcoders](https://github.com/arabcoders), [bashonly](https://github.com/bashonly)
- **viki**: [Remove extractors](https://github.com/yt-dlp/yt-dlp/commit/fe4f14b8369038e7c58f7de546d76de1ce3a91ce) ([#12703](https://github.com/yt-dlp/yt-dlp/issues/12703)) by [seproDev](https://github.com/seproDev)
- **vrsquare**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/b7fbb5a0a16a8e8d3e29c29e26ebed677d0d6ea3) ([#12515](https://github.com/yt-dlp/yt-dlp/issues/12515)) by [doe1080](https://github.com/doe1080)
- **youtube**
- [Fix PhantomJS nsig fallback](https://github.com/yt-dlp/yt-dlp/commit/4054a2b623bd1e277b49d2e9abc3d112a4b1c7be) ([#12728](https://github.com/yt-dlp/yt-dlp/issues/12728)) by [bashonly](https://github.com/bashonly)
- [Fix signature and nsig extraction for player `363db69b`](https://github.com/yt-dlp/yt-dlp/commit/b9c979461b244713bf42691a5bc02834e2ba4b2c) ([#12725](https://github.com/yt-dlp/yt-dlp/issues/12725)) by [bashonly](https://github.com/bashonly)
#### Networking changes
- **Request Handler**: curl_cffi: [Support `curl_cffi` 0.10.x](https://github.com/yt-dlp/yt-dlp/commit/9bf23902ceb948b9685ce1dab575491571720fc6) ([#12670](https://github.com/yt-dlp/yt-dlp/issues/12670)) by [Grub4K](https://github.com/Grub4K)
#### Misc. changes
- **cleanup**: Miscellaneous: [9dde546](https://github.com/yt-dlp/yt-dlp/commit/9dde546e7ee3e1515d88ee3af08b099351455dc0) by [seproDev](https://github.com/seproDev)
### 2025.03.21
#### Core changes
+3
View File
@@ -1866,6 +1866,9 @@ The following extractors use this feature:
#### sonylivseries
* `sort_order`: Episode sort order for series extraction - one of `asc` (ascending, oldest first) or `desc` (descending, newest first). Default is `asc`
#### tver
* `backend`: Backend API to use for extraction - one of `streaks` (default) or `brightcove` (deprecated)
**Note**: These options may be changed/removed in the future without concern for backward compatibility
<!-- MANPAGE: MOVE "INSTALLATION" SECTION HERE -->
+10 -8
View File
@@ -7,6 +7,7 @@ The only reliable way to check if a site is supported is to try it.
- **17live**
- **17live:clip**
- **17live:vod**
- **1News**: 1news.co.nz article videos
- **1tv**: Первый канал
- **20min**
@@ -200,7 +201,7 @@ The only reliable way to check if a site is supported is to try it.
- **blogger.com**
- **Bloomberg**
- **Bluesky**
- **BokeCC**
- **BokeCC**: CC视频
- **BongaCams**
- **Boosty**
- **BostonGlobe**
@@ -347,8 +348,6 @@ The only reliable way to check if a site is supported is to try it.
- **daystar:clip**
- **DBTV**
- **DctpTv**
- **DeezerAlbum**
- **DeezerPlaylist**
- **democracynow**
- **DestinationAmerica**
- **DetikEmbed**
@@ -829,7 +828,7 @@ The only reliable way to check if a site is supported is to try it.
- **MotherlessUploader**
- **Motorsport**: motorsport.com (**Currently broken**)
- **MovieFap**
- **Moviepilot**
- **moviepilot**: Moviepilot trailer
- **MoviewPlay**
- **Moviezine**
- **MovingImage**
@@ -1307,8 +1306,8 @@ The only reliable way to check if a site is supported is to try it.
- **sejm**
- **Sen**
- **SenalColombiaLive**: (**Currently broken**)
- **SenateGov**
- **SenateISVP**
- **senate.gov**
- **senate.gov:isvp**
- **SendtoNews**: (**Currently broken**)
- **Servus**
- **Sexu**: (**Currently broken**)
@@ -1401,6 +1400,7 @@ The only reliable way to check if a site is supported is to try it.
- **StoryFire**
- **StoryFireSeries**
- **StoryFireUser**
- **Streaks**
- **Streamable**
- **StreamCZ**
- **StreetVoice**
@@ -1643,8 +1643,6 @@ The only reliable way to check if a site is supported is to try it.
- **viewlift**
- **viewlift:embed**
- **Viidea**
- **viki**: [*viki*](## "netrc machine")
- **viki:channel**: [*viki*](## "netrc machine")
- **vimeo**: [*vimeo*](## "netrc machine")
- **vimeo:album**: [*vimeo*](## "netrc machine")
- **vimeo:channel**: [*vimeo*](## "netrc machine")
@@ -1682,6 +1680,10 @@ The only reliable way to check if a site is supported is to try it.
- **vpro**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
- **vqq:series**
- **vqq:video**
- **vrsquare**: VR SQUARE
- **vrsquare:channel**
- **vrsquare:search**
- **vrsquare:section**
- **VRT**: VRT NWS, Flanders News, Flandern Info and Sporza
- **vrtmax**: [*vrtnu*](## "netrc machine") VRT MAX (formerly VRT NU)
- **VTM**: (**Currently broken**)
+14 -8
View File
@@ -638,6 +638,7 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'img_bipbop_adv_example_fmp4',
'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
[{
# 60kbps (bitrate not provided in m3u8); sorted as worst because it's grouped with lowest bitrate video track
'format_id': 'aud1-English',
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/a1/prog_index.m3u8',
'manifest_url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
@@ -645,15 +646,9 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'ext': 'mp4',
'protocol': 'm3u8_native',
'audio_ext': 'mp4',
'source_preference': 0,
}, {
'format_id': 'aud2-English',
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/a2/prog_index.m3u8',
'manifest_url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
'language': 'en',
'ext': 'mp4',
'protocol': 'm3u8_native',
'audio_ext': 'mp4',
}, {
# 192kbps (bitrate not provided in m3u8)
'format_id': 'aud3-English',
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/a3/prog_index.m3u8',
'manifest_url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
@@ -661,6 +656,17 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
'ext': 'mp4',
'protocol': 'm3u8_native',
'audio_ext': 'mp4',
'source_preference': 1,
}, {
# 384kbps (bitrate not provided in m3u8); sorted as best because it's grouped with the highest bitrate video track
'format_id': 'aud2-English',
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/a2/prog_index.m3u8',
'manifest_url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8',
'language': 'en',
'ext': 'mp4',
'protocol': 'm3u8_native',
'audio_ext': 'mp4',
'source_preference': 2,
}, {
'format_id': '530',
'url': 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/v2/prog_index.m3u8',
+1
View File
@@ -1260,6 +1260,7 @@ class TestUtil(unittest.TestCase):
def test_js_to_json_malformed(self):
self.assertEqual(js_to_json('42a1'), '42"a1"')
self.assertEqual(js_to_json('42a-1'), '42"a"-1')
self.assertEqual(js_to_json('{a: `${e("")}`}'), '{"a": "\\"e\\"(\\"\\")"}')
def test_js_to_json_template_literal(self):
self.assertEqual(js_to_json('`Hello ${name}`', {'name': '"world"'}), '"Hello world"')
+9
View File
@@ -83,6 +83,11 @@ _SIG_TESTS = [
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
'AAOAOq0QJ8wRAIgXmPlOPSBkkUs1bYFYlJCfe29xx8j7vgpDL0QwbdV06sCIEzpWqMGkFR20CFOS21Tp-7vj_EMu-m37KtXJoOy1',
),
(
'https://www.youtube.com/s/player/363db69b/player_ias.vflset/en_US/base.js',
'2aq0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpzEICs69VdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
'0aqSyOoJXtK73m-uME_jv7-pT15gOFC02RFkGMqWpz2ICs6EVdbwQ0LDp1v7j8xx92efCJlYFYb1sUkkBSPOlPmXgIARw8JQ0qOAOAA',
),
]
_NSIG_TESTS = [
@@ -234,6 +239,10 @@ _NSIG_TESTS = [
'https://www.youtube.com/s/player/643afba4/tv-player-ias.vflset/tv-player-ias.js',
'ir9-V6cdbCiyKxhr', '2PL7ZDYAALMfmA',
),
(
'https://www.youtube.com/s/player/363db69b/player_ias.vflset/en_US/base.js',
'eWYu5d5YeY_4LyEDc', 'XJQqf-N7Xra3gg',
),
]
+8
View File
@@ -839,6 +839,7 @@ from .icareus import IcareusIE
from .ichinanalive import (
IchinanaLiveClipIE,
IchinanaLiveIE,
IchinanaLiveVODIE,
)
from .idolplus import IdolPlusIE
from .ign import (
@@ -1985,6 +1986,7 @@ from .storyfire import (
StoryFireSeriesIE,
StoryFireUserIE,
)
from .streaks import StreaksIE
from .streamable import StreamableIE
from .streamcz import StreamCZIE
from .streetvoice import StreetVoiceIE
@@ -2392,6 +2394,12 @@ from .voxmedia import (
VoxMediaIE,
VoxMediaVolumeIE,
)
from .vrsquare import (
VrSquareChannelIE,
VrSquareIE,
VrSquareSearchIE,
VrSquareSectionIE,
)
from .vrt import (
VRTIE,
DagelijkseKostIE,
+1 -1
View File
@@ -24,7 +24,7 @@ class BokeCCBaseIE(InfoExtractor):
class BokeCCIE(BokeCCBaseIE):
_IE_DESC = 'CC视频'
IE_DESC = 'CC视频'
_VALID_URL = r'https?://union\.bokecc\.com/playvideo\.bo\?(?P<query>.*)'
_TESTS = [{
+32 -1
View File
@@ -78,6 +78,7 @@ from ..utils import (
parse_iso8601,
parse_m3u8_attributes,
parse_resolution,
qualities,
sanitize_url,
smuggle_url,
str_or_none,
@@ -2177,6 +2178,8 @@ class InfoExtractor:
media_url = media.get('URI')
if media_url:
manifest_url = format_url(media_url)
is_audio = media_type == 'AUDIO'
is_alternate = media.get('DEFAULT') == 'NO' or media.get('AUTOSELECT') == 'NO'
formats.extend({
'format_id': join_nonempty(m3u8_id, group_id, name, idx),
'format_note': name,
@@ -2189,7 +2192,11 @@ class InfoExtractor:
'preference': preference,
'quality': quality,
'has_drm': has_drm,
'vcodec': 'none' if media_type == 'AUDIO' else None,
'vcodec': 'none' if is_audio else None,
# Alternate audio formats (e.g. audio description) should be deprioritized
'source_preference': -2 if is_audio and is_alternate else None,
# Save this to assign source_preference based on associated video stream
'_audio_group_id': group_id if is_audio and not is_alternate else None,
} for idx in _extract_m3u8_playlist_indices(manifest_url))
def build_stream_name():
@@ -2284,6 +2291,8 @@ class InfoExtractor:
# ignore references to rendition groups and treat them
# as complete formats.
if audio_group_id and codecs and f.get('vcodec') != 'none':
# Save this to determine quality of audio formats that only have a GROUP-ID
f['_audio_group_id'] = audio_group_id
audio_group = groups.get(audio_group_id)
if audio_group and audio_group[0].get('URI'):
# TODO: update acodec for audio only formats with
@@ -2306,6 +2315,28 @@ class InfoExtractor:
formats.append(http_f)
last_stream_inf = {}
# Some audio-only formats only have a GROUP-ID without any other quality/bitrate/codec info
# Each audio GROUP-ID corresponds with one or more video formats' AUDIO attribute
# For sorting purposes, set source_preference based on the quality of the video formats they are grouped with
# See https://github.com/yt-dlp/yt-dlp/issues/11178
audio_groups_by_quality = orderedSet(f['_audio_group_id'] for f in sorted(
traverse_obj(formats, lambda _, v: v.get('vcodec') != 'none' and v['_audio_group_id']),
key=lambda x: (x.get('tbr') or 0, x.get('width') or 0)))
audio_quality_map = {
audio_groups_by_quality[0]: 'low',
audio_groups_by_quality[-1]: 'high',
} if len(audio_groups_by_quality) > 1 else None
audio_preference = qualities(audio_groups_by_quality)
for fmt in formats:
audio_group_id = fmt.pop('_audio_group_id', None)
if not audio_quality_map or not audio_group_id or fmt.get('vcodec') != 'none':
continue
# Use source_preference since quality and preference are set by params
fmt['source_preference'] = audio_preference(audio_group_id)
fmt['format_note'] = join_nonempty(
fmt.get('format_note'), audio_quality_map.get(audio_group_id), delim=', ')
return formats, subtitles
def _extract_m3u8_vod_duration(
+2 -1
View File
@@ -16,6 +16,7 @@ from ..utils import (
MEDIA_EXTENSIONS,
ExtractorError,
UnsupportedError,
base_url,
determine_ext,
determine_protocol,
dict_get,
@@ -2531,7 +2532,7 @@ class GenericIE(InfoExtractor):
elif re.match(r'(?i)^(?:{[^}]+})?MPD$', doc.tag):
info_dict['formats'], info_dict['subtitles'] = self._parse_mpd_formats_and_subtitles(
doc,
mpd_base_url=full_response.url.rpartition('/')[0],
mpd_base_url=base_url(full_response.url),
mpd_url=url)
info_dict['live_status'] = 'is_live' if doc.get('type') == 'dynamic' else None
self._extra_manifest_info(info_dict, url)
+3 -3
View File
@@ -6,7 +6,7 @@ from ..utils import (
)
class HSEShowBaseInfoExtractor(InfoExtractor):
class HSEShowBaseIE(InfoExtractor):
_GEO_COUNTRIES = ['DE']
def _extract_redux_data(self, url, video_id):
@@ -28,7 +28,7 @@ class HSEShowBaseInfoExtractor(InfoExtractor):
return formats, subtitles
class HSEShowIE(HSEShowBaseInfoExtractor):
class HSEShowIE(HSEShowBaseIE):
_VALID_URL = r'https?://(?:www\.)?hse\.de/dpl/c/tv-shows/(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://www.hse.de/dpl/c/tv-shows/505350',
@@ -64,7 +64,7 @@ class HSEShowIE(HSEShowBaseInfoExtractor):
}
class HSEProductIE(HSEShowBaseInfoExtractor):
class HSEProductIE(HSEShowBaseIE):
_VALID_URL = r'https?://(?:www\.)?hse\.de/dpl/p/product/(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://www.hse.de/dpl/p/product/408630',
+57 -1
View File
@@ -1,5 +1,13 @@
from .common import InfoExtractor
from ..utils import ExtractorError, str_or_none, traverse_obj, unified_strdate
from ..utils import (
ExtractorError,
int_or_none,
str_or_none,
traverse_obj,
unified_strdate,
url_or_none,
)
class IchinanaLiveIE(InfoExtractor):
@@ -157,3 +165,51 @@ class IchinanaLiveClipIE(InfoExtractor):
'description': view_data.get('caption'),
'upload_date': unified_strdate(str_or_none(view_data.get('createdAt'))),
}
class IchinanaLiveVODIE(InfoExtractor):
IE_NAME = '17live:vod'
_VALID_URL = r'https?://(?:www\.)?17\.live/ja/vod/[^/?#]+/(?P<id>[^/?#]+)'
_TESTS = [{
'url': 'https://17.live/ja/vod/27323042/2cf84520-e65e-4b22-891e-1d3a00b0f068',
'md5': '3299b930d7457b069639486998a89580',
'info_dict': {
'id': '2cf84520-e65e-4b22-891e-1d3a00b0f068',
'ext': 'mp4',
'title': 'md5:b5f8cbf497d54cc6a60eb3b480182f01',
'uploader': 'md5:29fb12122ab94b5a8495586e7c3085a5',
'uploader_id': '27323042',
'channel': '🌟オールナイトニッポン アーカイブ🌟',
'channel_id': '2b4f85f1-d61e-429d-a901-68d32bdd8645',
'like_count': int,
'view_count': int,
'thumbnail': r're:https?://.+/.+\.(?:jpe?g|png)',
'duration': 549,
'description': 'md5:116f326579700f00eaaf5581aae1192e',
'timestamp': 1741058645,
'upload_date': '20250304',
},
}, {
'url': 'https://17.live/ja/vod/27323042/0de11bac-9bea-40b8-9eab-0239a7d88079',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
json_data = self._download_json(f'https://wap-api.17app.co/api/v1/vods/{video_id}', video_id)
return traverse_obj(json_data, {
'id': ('vodID', {str}),
'title': ('title', {str}),
'formats': ('vodURL', {lambda x: self._extract_m3u8_formats(x, video_id)}),
'uploader': ('userInfo', 'displayName', {str}),
'uploader_id': ('userInfo', 'roomID', {int}, {str_or_none}),
'channel': ('userInfo', 'name', {str}),
'channel_id': ('userInfo', 'userID', {str}),
'like_count': ('likeCount', {int_or_none}),
'view_count': ('viewCount', {int_or_none}),
'thumbnail': ('imageURL', {url_or_none}),
'duration': ('duration', {int_or_none}),
'description': ('description', {str}),
'timestamp': ('createdAt', {int_or_none}),
})
+2 -2
View File
@@ -3,8 +3,8 @@ from .dailymotion import DailymotionIE
class MoviepilotIE(InfoExtractor):
_IE_NAME = 'moviepilot'
_IE_DESC = 'Moviepilot trailer'
IE_NAME = 'moviepilot'
IE_DESC = 'Moviepilot trailer'
_VALID_URL = r'https?://(?:www\.)?moviepilot\.de/movies/(?P<id>[^/]+)'
_TESTS = [{
+6 -6
View File
@@ -22,7 +22,7 @@ from ..utils import (
)
class PolskieRadioBaseExtractor(InfoExtractor):
class PolskieRadioBaseIE(InfoExtractor):
def _extract_webpage_player_entries(self, webpage, playlist_id, base_data):
media_urls = set()
@@ -47,7 +47,7 @@ class PolskieRadioBaseExtractor(InfoExtractor):
yield entry
class PolskieRadioLegacyIE(PolskieRadioBaseExtractor):
class PolskieRadioLegacyIE(PolskieRadioBaseIE):
# legacy sites
IE_NAME = 'polskieradio:legacy'
_VALID_URL = r'https?://(?:www\.)?polskieradio(?:24)?\.pl/\d+/\d+/[Aa]rtykul/(?P<id>\d+)'
@@ -127,7 +127,7 @@ class PolskieRadioLegacyIE(PolskieRadioBaseExtractor):
return self.playlist_result(entries, playlist_id, title, description)
class PolskieRadioIE(PolskieRadioBaseExtractor):
class PolskieRadioIE(PolskieRadioBaseIE):
# new next.js sites
_VALID_URL = r'https?://(?:[^/]+\.)?(?:polskieradio(?:24)?|radiokierowcow)\.pl/artykul/(?P<id>\d+)'
_TESTS = [{
@@ -519,7 +519,7 @@ class PolskieRadioPlayerIE(InfoExtractor):
}
class PolskieRadioPodcastBaseExtractor(InfoExtractor):
class PolskieRadioPodcastBaseIE(InfoExtractor):
_API_BASE = 'https://apipodcasts.polskieradio.pl/api'
def _parse_episode(self, data):
@@ -539,7 +539,7 @@ class PolskieRadioPodcastBaseExtractor(InfoExtractor):
}
class PolskieRadioPodcastListIE(PolskieRadioPodcastBaseExtractor):
class PolskieRadioPodcastListIE(PolskieRadioPodcastBaseIE):
IE_NAME = 'polskieradio:podcast:list'
_VALID_URL = r'https?://podcasty\.polskieradio\.pl/podcast/(?P<id>\d+)'
_TESTS = [{
@@ -578,7 +578,7 @@ class PolskieRadioPodcastListIE(PolskieRadioPodcastBaseExtractor):
}
class PolskieRadioPodcastIE(PolskieRadioPodcastBaseExtractor):
class PolskieRadioPodcastIE(PolskieRadioPodcastBaseIE):
IE_NAME = 'polskieradio:podcast'
_VALID_URL = r'https?://podcasty\.polskieradio\.pl/track/(?P<id>[a-f\d]{8}(?:-[a-f\d]{4}){4}[a-f\d]{8})'
_TESTS = [{
+4 -4
View File
@@ -12,7 +12,7 @@ from ..utils import (
)
class RedGifsBaseInfoExtractor(InfoExtractor):
class RedGifsBaseIE(InfoExtractor):
_FORMATS = {
'gif': 250,
'sd': 480,
@@ -113,7 +113,7 @@ class RedGifsBaseInfoExtractor(InfoExtractor):
return page_fetcher(page) if page else OnDemandPagedList(page_fetcher, self._PAGE_SIZE)
class RedGifsIE(RedGifsBaseInfoExtractor):
class RedGifsIE(RedGifsBaseIE):
_VALID_URL = r'https?://(?:(?:www\.)?redgifs\.com/(?:watch|ifr)/|thumbs2\.redgifs\.com/)(?P<id>[^-/?#\.]+)'
_TESTS = [{
'url': 'https://www.redgifs.com/watch/squeakyhelplesswisent',
@@ -172,7 +172,7 @@ class RedGifsIE(RedGifsBaseInfoExtractor):
return self._parse_gif_data(video_info['gif'])
class RedGifsSearchIE(RedGifsBaseInfoExtractor):
class RedGifsSearchIE(RedGifsBaseIE):
IE_DESC = 'Redgifs search'
_VALID_URL = r'https?://(?:www\.)?redgifs\.com/browse\?(?P<query>[^#]+)'
_PAGE_SIZE = 80
@@ -226,7 +226,7 @@ class RedGifsSearchIE(RedGifsBaseInfoExtractor):
entries, query_str, tags, f'RedGifs search for {tags}, ordered by {order}')
class RedGifsUserIE(RedGifsBaseInfoExtractor):
class RedGifsUserIE(RedGifsBaseIE):
IE_DESC = 'Redgifs user'
_VALID_URL = r'https?://(?:www\.)?redgifs\.com/users/(?P<username>[^/?#]+)(?:\?(?P<query>[^#]+))?'
_PAGE_SIZE = 80
+2 -2
View File
@@ -13,7 +13,7 @@ from ..utils.traversal import traverse_obj
class SenateISVPIE(InfoExtractor):
_IE_NAME = 'senate.gov:isvp'
IE_NAME = 'senate.gov:isvp'
_VALID_URL = r'https?://(?:www\.)?senate\.gov/isvp/?\?(?P<qs>.+)'
_EMBED_REGEX = [r"<iframe[^>]+src=['\"](?P<url>https?://www\.senate\.gov/isvp/?\?[^'\"]+)['\"]"]
@@ -137,7 +137,7 @@ class SenateISVPIE(InfoExtractor):
class SenateGovIE(InfoExtractor):
_IE_NAME = 'senate.gov'
IE_NAME = 'senate.gov'
_SUBDOMAIN_RE = '|'.join(map(re.escape, (
'agriculture', 'aging', 'appropriations', 'armed-services', 'banking',
'budget', 'commerce', 'energy', 'epw', 'finance', 'foreign', 'help',
+236
View File
@@ -0,0 +1,236 @@
import json
import urllib.parse
from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
ExtractorError,
filter_dict,
float_or_none,
join_nonempty,
mimetype2ext,
parse_iso8601,
unsmuggle_url,
update_url_query,
url_or_none,
)
from ..utils.traversal import traverse_obj
class StreaksBaseIE(InfoExtractor):
_API_URL_TEMPLATE = 'https://{}.api.streaks.jp/v1/projects/{}/medias/{}{}'
_GEO_BYPASS = False
_GEO_COUNTRIES = ['JP']
def _extract_from_streaks_api(self, project_id, media_id, headers=None, query=None, ssai=False):
try:
response = self._download_json(
self._API_URL_TEMPLATE.format('playback', project_id, media_id, ''),
media_id, 'Downloading STREAKS playback API JSON', headers={
'Accept': 'application/json',
'Origin': 'https://players.streaks.jp',
**self.geo_verification_headers(),
**(headers or {}),
})
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status in {403, 404}:
error = self._parse_json(e.cause.response.read().decode(), media_id, fatal=False)
message = traverse_obj(error, ('message', {str}))
code = traverse_obj(error, ('code', {str}))
if code == 'REQUEST_FAILED':
self.raise_geo_restricted(message, countries=self._GEO_COUNTRIES)
elif code == 'MEDIA_NOT_FOUND':
raise ExtractorError(message, expected=True)
elif code or message:
raise ExtractorError(join_nonempty(code, message, delim=': '))
raise
streaks_id = response['id']
live_status = {
'clip': 'was_live',
'file': 'not_live',
'linear': 'is_live',
'live': 'is_live',
}.get(response.get('type'))
formats, subtitles = [], {}
drm_formats = False
for source in traverse_obj(response, ('sources', lambda _, v: v['src'])):
if source.get('key_systems'):
drm_formats = True
continue
src_url = source['src']
is_live = live_status == 'is_live'
ext = mimetype2ext(source.get('type'))
if ext != 'm3u8':
self.report_warning(f'Unsupported stream type: {ext}')
continue
if is_live and ssai:
session_params = traverse_obj(self._download_json(
self._API_URL_TEMPLATE.format('ssai', project_id, streaks_id, '/ssai/session'),
media_id, 'Downloading session parameters',
headers={'Content-Type': 'application/json', 'Accept': 'application/json'},
data=json.dumps({'id': source['id']}).encode(),
), (0, 'query', {urllib.parse.parse_qs}))
src_url = update_url_query(src_url, session_params)
fmts, subs = self._extract_m3u8_formats_and_subtitles(
src_url, media_id, 'mp4', m3u8_id='hls', fatal=False, live=is_live, query=query)
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
if not formats and drm_formats:
self.report_drm(media_id)
self._remove_duplicate_formats(formats)
for subs in traverse_obj(response, (
'tracks', lambda _, v: v['kind'] in ('captions', 'subtitles') and url_or_none(v['src']),
)):
lang = traverse_obj(subs, ('srclang', {str.lower})) or 'ja'
subtitles.setdefault(lang, []).append({'url': subs['src']})
return {
'id': streaks_id,
'display_id': media_id,
'formats': formats,
'live_status': live_status,
'subtitles': subtitles,
'uploader_id': project_id,
**traverse_obj(response, {
'title': ('name', {str}),
'description': ('description', {str}, filter),
'duration': ('duration', {float_or_none}),
'modified_timestamp': ('updated_at', {parse_iso8601}),
'tags': ('tags', ..., {str}),
'thumbnails': (('poster', 'thumbnail'), 'src', {'url': {url_or_none}}),
'timestamp': ('created_at', {parse_iso8601}),
}),
}
class StreaksIE(StreaksBaseIE):
_VALID_URL = [
r'https?://players\.streaks\.jp/(?P<project_id>[\w-]+)/[\da-f]+/index\.html\?(?:[^#]+&)?m=(?P<id>(?:ref:)?[\w-]+)',
r'https?://playback\.api\.streaks\.jp/v1/projects/(?P<project_id>[\w-]+)/medias/(?P<id>(?:ref:)?[\w-]+)',
]
_EMBED_REGEX = [rf'<iframe\s+[^>]*\bsrc\s*=\s*["\'](?P<url>{_VALID_URL[0]})']
_TESTS = [{
'url': 'https://players.streaks.jp/tipness/08155cd19dc14c12bebefb69b92eafcc/index.html?m=dbdf2df35b4d483ebaeeaeb38c594647',
'info_dict': {
'id': 'dbdf2df35b4d483ebaeeaeb38c594647',
'ext': 'mp4',
'title': '3shunenCM_edit.mp4',
'display_id': 'dbdf2df35b4d483ebaeeaeb38c594647',
'duration': 47.533,
'live_status': 'not_live',
'modified_date': '20230726',
'modified_timestamp': 1690356180,
'timestamp': 1690355996,
'upload_date': '20230726',
'uploader_id': 'tipness',
},
}, {
'url': 'https://players.streaks.jp/ktv-web/0298e8964c164ab384c07ef6e08c444b/index.html?m=ref:mycoffeetime_250317',
'info_dict': {
'id': 'dccdc079e3fd41f88b0c8435e2d453ab',
'ext': 'mp4',
'title': 'わたしの珈琲時間_250317',
'display_id': 'ref:mycoffeetime_250317',
'duration': 122.99,
'live_status': 'not_live',
'modified_date': '20250310',
'modified_timestamp': 1741586302,
'thumbnail': r're:https?://.+\.jpg',
'timestamp': 1741585839,
'upload_date': '20250310',
'uploader_id': 'ktv-web',
},
}, {
'url': 'https://playback.api.streaks.jp/v1/projects/ktv-web/medias/b5411938e1e5435dac71edf829dd4813',
'info_dict': {
'id': 'b5411938e1e5435dac71edf829dd4813',
'ext': 'mp4',
'title': 'KANTELE_SYUSEi_0630',
'display_id': 'b5411938e1e5435dac71edf829dd4813',
'live_status': 'not_live',
'modified_date': '20250122',
'modified_timestamp': 1737522999,
'thumbnail': r're:https?://.+\.jpg',
'timestamp': 1735205137,
'upload_date': '20241226',
'uploader_id': 'ktv-web',
},
}, {
# TVer Olympics: website already down, but api remains accessible
'url': 'https://playback.api.streaks.jp/v1/projects/tver-olympic/medias/ref:sp_240806_1748_dvr',
'info_dict': {
'id': 'c10f7345adb648cf804d7578ab93b2e3',
'ext': 'mp4',
'title': 'サッカー 男子 準決勝_dvr',
'display_id': 'ref:sp_240806_1748_dvr',
'duration': 12960.0,
'live_status': 'was_live',
'modified_date': '20240805',
'modified_timestamp': 1722896263,
'timestamp': 1722777618,
'upload_date': '20240804',
'uploader_id': 'tver-olympic',
},
}, {
# TBS FREE: 24-hour stream
'url': 'https://playback.api.streaks.jp/v1/projects/tbs/medias/ref:simul-02',
'info_dict': {
'id': 'c4e83a7b48f4409a96adacec674b4e22',
'ext': 'mp4',
'title': str,
'display_id': 'ref:simul-02',
'live_status': 'is_live',
'modified_date': '20241031',
'modified_timestamp': 1730339858,
'timestamp': 1705466840,
'upload_date': '20240117',
'uploader_id': 'tbs',
},
}, {
# DRM protected
'url': 'https://players.streaks.jp/sp-jbc/a12d7ee0f40c49d6a0a2bff520639677/index.html?m=5f89c62f37ee4a68be8e6e3b1396c7d8',
'only_matching': True,
}]
_WEBPAGE_TESTS = [{
'url': 'https://event.play.jp/playnext2023/',
'info_dict': {
'id': '2d975178293140dc8074a7fc536a7604',
'ext': 'mp4',
'title': 'PLAY NEXTキームービー(本番)',
'uploader_id': 'play',
'duration': 17.05,
'thumbnail': r're:https?://.+\.jpg',
'timestamp': 1668387517,
'upload_date': '20221114',
'modified_timestamp': 1739411523,
'modified_date': '20250213',
'live_status': 'not_live',
},
}, {
'url': 'https://wowshop.jp/Page/special/cooking_goods/?bid=wowshop&srsltid=AfmBOor_phUNoPEE_UCPiGGSCMrJE5T2US397smvsbrSdLqUxwON0el4',
'playlist_mincount': 2,
'info_dict': {
'id': '?bid=wowshop&srsltid=AfmBOor_phUNoPEE_UCPiGGSCMrJE5T2US397smvsbrSdLqUxwON0el4',
'title': 'ワンランク上の料理道具でとびきりの“おいしい”を食卓へ|wowshop',
'description': 'md5:914b5cb8624fc69274c7fb7b2342958f',
'age_limit': 0,
'thumbnail': 'https://wowshop.jp/Page/special/cooking_goods/images/ogp.jpg',
},
}]
def _real_extract(self, url):
url, smuggled_data = unsmuggle_url(url, {})
project_id, media_id = self._match_valid_url(url).group('project_id', 'id')
return self._extract_from_streaks_api(
project_id, media_id, headers=filter_dict({
'X-Streaks-Api-Key': smuggled_data.get('api_key'),
}))
+3 -3
View File
@@ -191,12 +191,12 @@ class TapTapAppIE(TapTapBaseIE):
}]
class TapTapIntlBase(TapTapBaseIE):
class TapTapIntlBaseIE(TapTapBaseIE):
_X_UA = 'V=1&PN=WebAppIntl2&LANG=zh_TW&VN_CODE=115&VN=0.1.0&LOC=CN&PLT=PC&DS=Android&UID={uuid}&CURR=&DT=PC&OS=Windows&OSV=NT%208.0.0'
_VIDEO_API = 'https://www.taptap.io/webapiv2/video-resource/v1/multi-get'
class TapTapAppIntlIE(TapTapIntlBase):
class TapTapAppIntlIE(TapTapIntlBaseIE):
_VALID_URL = r'https?://www\.taptap\.io/app/(?P<id>\d+)'
_INFO_API = 'https://www.taptap.io/webapiv2/i/app/v5/detail'
_DATA_PATH = 'app'
@@ -227,7 +227,7 @@ class TapTapAppIntlIE(TapTapIntlBase):
}]
class TapTapPostIntlIE(TapTapIntlBase):
class TapTapPostIntlIE(TapTapIntlBaseIE):
_VALID_URL = r'https?://www\.taptap\.io/post/(?P<id>\d+)'
_INFO_API = 'https://www.taptap.io/webapiv2/creation/post/v1/detail'
_INFO_QUERY_KEY = 'id_str'
+103 -46
View File
@@ -1,31 +1,70 @@
from .common import InfoExtractor
from .streaks import StreaksBaseIE
from ..utils import (
ExtractorError,
int_or_none,
join_nonempty,
make_archive_id,
smuggle_url,
str_or_none,
strip_or_none,
traverse_obj,
update_url_query,
)
from ..utils.traversal import require, traverse_obj
class TVerIE(InfoExtractor):
class TVerIE(StreaksBaseIE):
_VALID_URL = r'https?://(?:www\.)?tver\.jp/(?:(?P<type>lp|corner|series|episodes?|feature)/)+(?P<id>[a-zA-Z0-9]+)'
_GEO_COUNTRIES = ['JP']
_GEO_BYPASS = False
_TESTS = [{
'skip': 'videos are only available for 7 days',
'url': 'https://tver.jp/episodes/ep83nf3w4p',
# via Streaks backend
'url': 'https://tver.jp/episodes/epc1hdugbk',
'info_dict': {
'title': '家事ヤロウ!!! 売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
'description': 'md5:dc2c06b6acc23f1e7c730c513737719b',
'series': '家事ヤロウ!!!',
'episode': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
'alt_title': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
'channel': 'テレビ朝日',
'id': 'ep83nf3w4p',
'id': 'epc1hdugbk',
'ext': 'mp4',
'display_id': 'ref:baeebeac-a2a6-4dbf-9eb3-c40d59b40068',
'title': '神回だけ見せます! #2 壮烈!車大騎馬戦(木曜スペシャル)',
'alt_title': '神回だけ見せます! #2 壮烈!車大騎馬戦(木曜スペシャル) 日テレ',
'description': 'md5:2726f742d5e3886edeaf72fb6d740fef',
'uploader_id': 'tver-ntv',
'channel': '日テレ',
'duration': 1158.024,
'thumbnail': 'https://statics.tver.jp/images/content/thumbnail/episode/xlarge/epc1hdugbk.jpg?v=16',
'series': '神回だけ見せます!',
'episode': '#2 壮烈!車大騎馬戦(木曜スペシャル)',
'episode_number': 2,
'timestamp': 1736486036,
'upload_date': '20250110',
'modified_timestamp': 1736870264,
'modified_date': '20250114',
'live_status': 'not_live',
'release_timestamp': 1651453200,
'release_date': '20220502',
'_old_archive_ids': ['brightcovenew ref:baeebeac-a2a6-4dbf-9eb3-c40d59b40068'],
},
'add_ie': ['BrightcoveNew'],
}, {
# via Brightcove backend (deprecated)
'url': 'https://tver.jp/episodes/epc1hdugbk',
'info_dict': {
'id': 'ref:baeebeac-a2a6-4dbf-9eb3-c40d59b40068',
'ext': 'mp4',
'title': '神回だけ見せます! #2 壮烈!車大騎馬戦(木曜スペシャル)',
'alt_title': '神回だけ見せます! #2 壮烈!車大騎馬戦(木曜スペシャル) 日テレ',
'description': 'md5:2726f742d5e3886edeaf72fb6d740fef',
'uploader_id': '4394098882001',
'channel': '日テレ',
'duration': 1158.101,
'thumbnail': 'https://statics.tver.jp/images/content/thumbnail/episode/xlarge/epc1hdugbk.jpg?v=16',
'tags': [],
'series': '神回だけ見せます!',
'episode': '#2 壮烈!車大騎馬戦(木曜スペシャル)',
'episode_number': 2,
'timestamp': 1651388531,
'upload_date': '20220501',
'release_timestamp': 1651453200,
'release_date': '20220502',
},
'params': {'extractor_args': {'tver': {'backend': ['brightcove']}}},
}, {
'url': 'https://tver.jp/corner/f0103888',
'only_matching': True,
@@ -38,26 +77,7 @@ class TVerIE(InfoExtractor):
'id': 'srtxft431v',
'title': '名探偵コナン',
},
'playlist': [
{
'md5': '779ffd97493ed59b0a6277ea726b389e',
'info_dict': {
'id': 'ref:conan-1137-241005',
'ext': 'mp4',
'title': '名探偵コナン #1137「行列店、味変の秘密」',
'uploader_id': '5330942432001',
'tags': [],
'channel': '読売テレビ',
'series': '名探偵コナン',
'description': 'md5:601fccc1d2430d942a2c8068c4b33eb5',
'episode': '#1137「行列店、味変の秘密」',
'duration': 1469.077,
'timestamp': 1728030405,
'upload_date': '20241004',
'alt_title': '名探偵コナン #1137「行列店、味変の秘密」 読売テレビ 10月5日(土)放送分',
'thumbnail': r're:https://.+\.jpg',
},
}],
'playlist_mincount': 21,
}, {
'url': 'https://tver.jp/series/sru35hwdd2',
'info_dict': {
@@ -70,7 +90,11 @@ class TVerIE(InfoExtractor):
'only_matching': True,
}]
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'
_HEADERS = {'x-tver-platform-type': 'web'}
_HEADERS = {
'x-tver-platform-type': 'web',
'Origin': 'https://tver.jp',
'Referer': 'https://tver.jp/',
}
_PLATFORM_QUERY = {}
def _real_initialize(self):
@@ -103,6 +127,9 @@ class TVerIE(InfoExtractor):
def _real_extract(self, url):
video_id, video_type = self._match_valid_url(url).group('id', 'type')
backend = self._configuration_arg('backend', ['streaks'])[0]
if backend not in ('brightcove', 'streaks'):
raise ExtractorError(f'Invalid backend value: {backend}', expected=True)
if video_type == 'series':
series_info = self._call_platform_api(
@@ -129,12 +156,6 @@ class TVerIE(InfoExtractor):
video_info = self._download_json(
f'https://statics.tver.jp/content/episode/{video_id}.json', video_id, 'Downloading video info',
query={'v': version}, headers={'Referer': 'https://tver.jp/'})
p_id = video_info['video']['accountID']
r_id = traverse_obj(video_info, ('video', ('videoRefID', 'videoID')), get_all=False)
if not r_id:
raise ExtractorError('Failed to extract reference ID for Brightcove')
if not r_id.isdigit():
r_id = f'ref:{r_id}'
episode = strip_or_none(episode_content.get('title'))
series = str_or_none(episode_content.get('seriesTitle'))
@@ -161,17 +182,53 @@ class TVerIE(InfoExtractor):
]
]
return {
'_type': 'url_transparent',
metadata = {
'title': title,
'series': series,
'episode': episode,
# an another title which is considered "full title" for some viewers
'alt_title': join_nonempty(title, provider, onair_label, delim=' '),
'channel': provider,
'description': str_or_none(video_info.get('description')),
'thumbnails': thumbnails,
'url': smuggle_url(
self.BRIGHTCOVE_URL_TEMPLATE % (p_id, r_id), {'geo_countries': ['JP']}),
'ie_key': 'BrightcoveNew',
**traverse_obj(video_info, {
'description': ('description', {str}),
'release_timestamp': ('viewStatus', 'startAt', {int_or_none}),
'episode_number': ('no', {int_or_none}),
}),
}
brightcove_id = traverse_obj(video_info, ('video', ('videoRefID', 'videoID'), {str}, any))
if brightcove_id and not brightcove_id.isdecimal():
brightcove_id = f'ref:{brightcove_id}'
streaks_id = traverse_obj(video_info, ('streaks', 'videoRefID', {str}))
if streaks_id and not streaks_id.startswith('ref:'):
streaks_id = f'ref:{streaks_id}'
# Deprecated Brightcove extraction reachable w/extractor-arg or fallback; errors are expected
if backend == 'brightcove' or not streaks_id:
if backend != 'brightcove':
self.report_warning(
'No STREAKS ID found; falling back to Brightcove extraction', video_id=video_id)
if not brightcove_id:
raise ExtractorError('Unable to extract brightcove reference ID', expected=True)
account_id = traverse_obj(video_info, (
'video', 'accountID', {str}, {require('brightcove account ID', expected=True)}))
return {
**metadata,
'_type': 'url_transparent',
'url': smuggle_url(
self.BRIGHTCOVE_URL_TEMPLATE % (account_id, brightcove_id),
{'geo_countries': ['JP']}),
'ie_key': 'BrightcoveNew',
}
return {
**self._extract_from_streaks_api(video_info['streaks']['projectID'], streaks_id, {
'Origin': 'https://tver.jp',
'Referer': 'https://tver.jp/',
}),
**metadata,
'id': video_id,
'_old_archive_ids': [make_archive_id('BrightcoveNew', brightcove_id)] if brightcove_id else None,
}
+185
View File
@@ -0,0 +1,185 @@
import itertools
from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
ExtractorError,
clean_html,
extract_attributes,
parse_duration,
parse_qs,
)
from ..utils.traversal import (
find_element,
find_elements,
traverse_obj,
)
class VrSquareIE(InfoExtractor):
IE_NAME = 'vrsquare'
IE_DESC = 'VR SQUARE'
_BASE_URL = 'https://livr.jp'
_VALID_URL = r'https?://livr\.jp/contents/(?P<id>[\w-]+)'
_TESTS = [{
'url': 'https://livr.jp/contents/P470896661',
'info_dict': {
'id': 'P470896661',
'ext': 'mp4',
'title': 'そこ曲がったら、櫻坂? 7年間お疲れ様!菅井友香の卒業を祝う会!前半 2022年11月6日放送分',
'description': 'md5:523726dc835aa8014dfe1e2b38d36cd1',
'duration': 1515.0,
'tags': 'count:2',
'thumbnail': r're:https?://media\.livr\.jp/vod/img/.+\.jpg',
},
}, {
'url': 'https://livr.jp/contents/P589523973',
'info_dict': {
'id': 'P589523973',
'ext': 'mp4',
'title': '薄闇に仰ぐ しだれ桜の妖艶',
'description': 'md5:a042f517b2cbb4ed6746707afec4d306',
'duration': 1084.0,
'tags': list,
'thumbnail': r're:https?://media\.livr\.jp/vod/img/.+\.jpg',
},
'skip': 'Paid video',
}, {
'url': 'https://livr.jp/contents/P316939908',
'info_dict': {
'id': 'P316939908',
'ext': 'mp4',
'title': '2024年5月16日(木) 「今日は誰に恋をする?」公演 小栗有以 生誕祭',
'description': 'md5:2110bdcf947f28bd7d06ec420e51b619',
'duration': 8559.0,
'tags': list,
'thumbnail': r're:https?://media\.livr\.jp/vod/img/.+\.jpg',
},
'skip': 'Premium channel subscribers only',
}, {
# Accessible only in the VR SQUARE app
'url': 'https://livr.jp/contents/P126481458',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
status = self._download_json(
f'{self._BASE_URL}/webApi/contentsStatus/{video_id}',
video_id, 'Checking contents status', fatal=False)
if traverse_obj(status, 'result_code') == '40407':
self.raise_login_required('Unable to access this video')
try:
web_api = self._download_json(
f'{self._BASE_URL}/webApi/play/url/{video_id}', video_id)
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 500:
raise ExtractorError('VR SQUARE app-only videos are not supported', expected=True)
raise
return {
'id': video_id,
'title': self._html_search_meta(['og:title', 'twitter:title'], webpage),
'description': self._html_search_meta('description', webpage),
'formats': self._extract_m3u8_formats(traverse_obj(web_api, (
'urls', ..., 'url', any)), video_id, 'mp4', fatal=False),
'thumbnail': self._html_search_meta('og:image', webpage),
**traverse_obj(webpage, {
'duration': ({find_element(cls='layout-product-data-time')}, {parse_duration}),
'tags': ({find_elements(cls='search-tag')}, ..., {clean_html}),
}),
}
class VrSquarePlaylistBaseIE(InfoExtractor):
_BASE_URL = 'https://livr.jp'
def _fetch_vids(self, source, keys=()):
for url_path in traverse_obj(source, (
*keys, {find_elements(cls='video', html=True)}, ...,
{extract_attributes}, 'data-url', {str}, filter),
):
yield self.url_result(
f'{self._BASE_URL}/contents/{url_path.removeprefix("/contents/")}', VrSquareIE)
def _entries(self, path, display_id, query=None):
for page in itertools.count(1):
ajax = self._download_json(
f'{self._BASE_URL}{path}', display_id,
f'Downloading playlist JSON page {page}',
query={'p': page, **(query or {})})
yield from self._fetch_vids(ajax, ('contents_render_list', ...))
if not traverse_obj(ajax, (('has_next', 'hasNext'), {bool}, any)):
break
class VrSquareChannelIE(VrSquarePlaylistBaseIE):
IE_NAME = 'vrsquare:channel'
_VALID_URL = r'https?://livr\.jp/channel/(?P<id>\w+)'
_TESTS = [{
'url': 'https://livr.jp/channel/H372648599',
'info_dict': {
'id': 'H372648599',
'title': 'AKB48+チャンネル',
},
'playlist_mincount': 502,
}]
def _real_extract(self, url):
playlist_id = self._match_id(url)
webpage = self._download_webpage(url, playlist_id)
return self.playlist_result(
self._entries(f'/ajax/channel/{playlist_id}', playlist_id),
playlist_id, self._html_search_meta('og:title', webpage))
class VrSquareSearchIE(VrSquarePlaylistBaseIE):
IE_NAME = 'vrsquare:search'
_VALID_URL = r'https?://livr\.jp/web-search/?\?(?:[^#]+&)?w=[^#]+'
_TESTS = [{
'url': 'https://livr.jp/web-search?w=%23%E5%B0%8F%E6%A0%97%E6%9C%89%E4%BB%A5',
'info_dict': {
'id': '#小栗有以',
},
'playlist_mincount': 60,
}]
def _real_extract(self, url):
search_query = parse_qs(url)['w'][0]
return self.playlist_result(
self._entries('/ajax/web-search', search_query, {'w': search_query}), search_query)
class VrSquareSectionIE(VrSquarePlaylistBaseIE):
IE_NAME = 'vrsquare:section'
_VALID_URL = r'https?://livr\.jp/(?:category|headline)/(?P<id>\w+)'
_TESTS = [{
'url': 'https://livr.jp/category/C133936275',
'info_dict': {
'id': 'C133936275',
'title': 'そこ曲がったら、櫻坂?VR',
},
'playlist_mincount': 308,
}, {
'url': 'https://livr.jp/headline/A296449604',
'info_dict': {
'id': 'A296449604',
'title': 'AKB48 アフターVR',
},
'playlist_mincount': 22,
}]
def _real_extract(self, url):
playlist_id = self._match_id(url)
webpage = self._download_webpage(url, playlist_id)
return self.playlist_result(
self._fetch_vids(webpage), playlist_id, self._html_search_meta('og:title', webpage))
+5 -5
View File
@@ -11,7 +11,7 @@ from ..utils import (
)
class WykopBaseExtractor(InfoExtractor):
class WykopBaseIE(InfoExtractor):
def _get_token(self, force_refresh=False):
if not force_refresh:
maybe_cached = self.cache.load('wykop', 'bearer')
@@ -72,7 +72,7 @@ class WykopBaseExtractor(InfoExtractor):
}
class WykopDigIE(WykopBaseExtractor):
class WykopDigIE(WykopBaseIE):
IE_NAME = 'wykop:dig'
_VALID_URL = r'https?://(?:www\.)?wykop\.pl/link/(?P<id>\d+)'
@@ -128,7 +128,7 @@ class WykopDigIE(WykopBaseExtractor):
}
class WykopDigCommentIE(WykopBaseExtractor):
class WykopDigCommentIE(WykopBaseIE):
IE_NAME = 'wykop:dig:comment'
_VALID_URL = r'https?://(?:www\.)?wykop\.pl/link/(?P<dig_id>\d+)/[^/]+/komentarz/(?P<id>\d+)'
@@ -177,7 +177,7 @@ class WykopDigCommentIE(WykopBaseExtractor):
}
class WykopPostIE(WykopBaseExtractor):
class WykopPostIE(WykopBaseIE):
IE_NAME = 'wykop:post'
_VALID_URL = r'https?://(?:www\.)?wykop\.pl/wpis/(?P<id>\d+)'
@@ -228,7 +228,7 @@ class WykopPostIE(WykopBaseExtractor):
}
class WykopPostCommentIE(WykopBaseExtractor):
class WykopPostCommentIE(WykopBaseIE):
IE_NAME = 'wykop:post:comment'
_VALID_URL = r'https?://(?:www\.)?wykop\.pl/wpis/(?P<post_id>\d+)/[^/#]+#(?P<id>\d+)'
+7 -7
View File
@@ -227,7 +227,7 @@ class YouPornIE(InfoExtractor):
return result
class YouPornListBase(InfoExtractor):
class YouPornListBaseIE(InfoExtractor):
def _get_next_url(self, url, pl_id, html):
return urljoin(url, self._search_regex(
r'''<a [^>]*?\bhref\s*=\s*("|')(?P<url>(?:(?!\1)[^>])+)\1''',
@@ -284,7 +284,7 @@ class YouPornListBase(InfoExtractor):
playlist_id=pl_id, playlist_title=title)
class YouPornCategoryIE(YouPornListBase):
class YouPornCategoryIE(YouPornListBaseIE):
IE_DESC = 'YouPorn category, with sorting, filtering and pagination'
_VALID_URL = r'''(?x)
https?://(?:www\.)?youporn\.com/
@@ -319,7 +319,7 @@ class YouPornCategoryIE(YouPornListBase):
}]
class YouPornChannelIE(YouPornListBase):
class YouPornChannelIE(YouPornListBaseIE):
IE_DESC = 'YouPorn channel, with sorting and pagination'
_VALID_URL = r'''(?x)
https?://(?:www\.)?youporn\.com/
@@ -349,7 +349,7 @@ class YouPornChannelIE(YouPornListBase):
return re.sub(r'_', ' ', title_slug).title()
class YouPornCollectionIE(YouPornListBase):
class YouPornCollectionIE(YouPornListBaseIE):
IE_DESC = 'YouPorn collection (user playlist), with sorting and pagination'
_VALID_URL = r'''(?x)
https?://(?:www\.)?youporn\.com/
@@ -394,7 +394,7 @@ class YouPornCollectionIE(YouPornListBase):
return playlist
class YouPornTagIE(YouPornListBase):
class YouPornTagIE(YouPornListBaseIE):
IE_DESC = 'YouPorn tag (porntags), with sorting, filtering and pagination'
_VALID_URL = r'''(?x)
https?://(?:www\.)?youporn\.com/
@@ -442,7 +442,7 @@ class YouPornTagIE(YouPornListBase):
return super()._real_extract(url)
class YouPornStarIE(YouPornListBase):
class YouPornStarIE(YouPornListBaseIE):
IE_DESC = 'YouPorn Pornstar, with description, sorting and pagination'
_VALID_URL = r'''(?x)
https?://(?:www\.)?youporn\.com/
@@ -493,7 +493,7 @@ class YouPornStarIE(YouPornListBase):
}
class YouPornVideosIE(YouPornListBase):
class YouPornVideosIE(YouPornListBaseIE):
IE_DESC = 'YouPorn video (browse) playlists, with sorting, filtering and pagination'
_VALID_URL = r'''(?x)
https?://(?:www\.)?youporn\.com/
+7 -4
View File
@@ -2176,10 +2176,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
"""Returns tuple of strings: variable assignment code, variable name, variable value code"""
return self._search_regex(
r'''(?x)
\'use\s+strict\';\s*
(?P<q1>["\'])use\s+strict(?P=q1);\s*
(?P<code>
var\s+(?P<name>[a-zA-Z0-9_$]+)\s*=\s*
(?P<value>"(?:[^"\\]|\\.)+"\.split\("[^"]+"\))
(?P<value>
(?P<q2>["\'])(?:(?!(?P=q2)).|\\.)+(?P=q2)
\.split\((?P<q3>["\'])(?:(?!(?P=q3)).)+(?P=q3)\)
)
)[;,]
''', jscode, 'global variable', group=('code', 'name', 'value'), default=(None, None, None))
@@ -2187,7 +2190,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
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
code = global_var + '; ' + code
else:
self.write_debug('No global array variable found in player JS')
return argnames, re.sub(
@@ -2196,7 +2199,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
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.03.21')
func_code = self.cache.load('youtube-nsig', player_id, min_ver='2025.03.25')
jscode = func_code or self._load_player(video_id, player_url)
jsi = JSInterpreter(jscode)
+2 -1
View File
@@ -2767,7 +2767,8 @@ def js_to_json(code, vars={}, *, strict=False):
def template_substitute(match):
evaluated = js_to_json(match.group(1), vars, strict=strict)
if evaluated[0] == '"':
return json.loads(evaluated)
with contextlib.suppress(json.JSONDecodeError):
return json.loads(evaluated)
return evaluated
def fix_kv(m):
+3 -3
View File
@@ -1,8 +1,8 @@
# Autogenerated by devscripts/update-version.py
__version__ = '2025.03.21'
__version__ = '2025.03.25'
RELEASE_GIT_HEAD = 'f36e4b6e65cb8403791aae2f520697115cb88dec'
RELEASE_GIT_HEAD = '9dde546e7ee3e1515d88ee3af08b099351455dc0'
VARIANT = None
@@ -12,4 +12,4 @@ CHANNEL = 'stable'
ORIGIN = 'yt-dlp/yt-dlp'
_pkg_version = '2025.03.21'
_pkg_version = '2025.03.25'