<Project-20 YT-DLP> 给视频网站下载工具 yt-dlp/yt-dlp 加个页面 python web

介绍 yt-dlp

Github 项目:https://github.com/yt-dlp/yt-dlp

A feature-rich command-line audio/video downloader

一个功能丰富的视频与音频命令行下载器

原因与功能

之前我用的 cobalt 因为它不再提供Client Web功能,只能去它的官网使用。 翻 reddit 找到这个 YT-DLP,但它是个命令行工具,考虑参数大多很少用到,给它加个web 壳子,又可以放到docker里面运行。

在网页填入url,只列出含有视频+音频的文件。点下载后,文件可以保存在本地。命令的运行输出也在页面上显示。占用端口: 9012

YT-DLP 程序

代码在 Claude AI 帮助下完成,前端全靠它,Nice~ 

界面

目录结构

20.YT-DLP/
├── Dockerfile
├── app.py
├── static/
│   ├── css/
│   │   └── style.css
│   └── js/
│       └── script.js
├── templates/
│   └── index.html
└── temp_downloads/

完整代码

1. app.py

# app.py
from flask import Flask, render_template, request, jsonify, send_file
import yt_dlp
import os
import shutil
from werkzeug.utils import secure_filename
import time
import logging
import queue
from datetime import datetime
import sysapp = Flask(__name__)# 创建固定的临时目录
TEMP_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'temp_downloads')
if not os.path.exists(TEMP_DIR):os.makedirs(TEMP_DIR)# 存储下载信息的字典
DOWNLOADS = {}# 创建日志队列
log_queue = queue.Queue(maxsize=1000)class QueueHandler(logging.Handler):def __init__(self, log_queue):super().__init__()self.log_queue = log_queuedef emit(self, record):try:# 过滤掉 Werkzeug 的常规访问日志if record.name == 'werkzeug' and any(x in record.getMessage() for x in ['127.0.0.1','GET /api/logs','GET /static/','"GET / HTTP/1.1"']):return# 清理消息格式msg = self.format(record)if record.name == 'app':# 移除 "INFO:app:" 等前缀msg = msg.split(' - ')[-1]log_entry = {'timestamp': datetime.fromtimestamp(record.created).isoformat(),'message': msg,'level': record.levelname.lower(),'logger': record.name}# 如果队列满了,移除最旧的日志if self.log_queue.full():try:self.log_queue.get_nowait()except queue.Empty:passself.log_queue.put(log_entry)except Exception as e:print(f"Error in QueueHandler: {e}")# 配置日志格式
log_formatter = logging.Formatter('%(message)s')# 配置队列处理器
queue_handler = QueueHandler(log_queue)
queue_handler.setFormatter(log_formatter)# 配置控制台处理器
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(log_formatter)# 配置 Flask 日志
app.logger.handlers = []
app.logger.addHandler(queue_handler)
app.logger.addHandler(console_handler)
app.logger.setLevel(logging.INFO)# Werkzeug 日志只输出错误
werkzeug_logger = logging.getLogger('werkzeug')
werkzeug_logger.handlers = []
werkzeug_logger.addHandler(console_handler)
werkzeug_logger.setLevel(logging.WARNING)def cleanup_old_files():"""清理超过10分钟的临时文件"""current_time = time.time()for token, info in list(DOWNLOADS.items()):if current_time - info['timestamp'] > 600:  # 10分钟try:file_path = info['file_path']if os.path.exists(file_path):os.remove(file_path)del DOWNLOADS[token]except Exception as e:app.logger.error(f"清理文件失败: {str(e)}")def get_video_info(url):"""获取视频信息,包括可用的格式"""ydl_opts = {'quiet': True,'no_warnings': True,'format': None,'youtube_include_dash_manifest': True,'format_sort': ['res:2160',  # 4K'res:1440',  # 2K'res:1080',  # 1080p'res:720',   # 720p'res:480',   # 480p'fps:60',    # 优先60fps'fps',       # 然后是其他fps'vcodec:h264',  # 优先H.264编码'vcodec:vp9',   # 然后是VP9'acodec'      # 最后是音频编码]}with yt_dlp.YoutubeDL(ydl_opts) as ydl:try:info = ydl.extract_info(url, download=False)formats = []def safe_number(value, default=0):try:return float(value or default)except (TypeError, ValueError):return default# 处理视频格式for f in info.get('formats', []):vcodec = f.get('vcodec', 'none')acodec = f.get('acodec', 'none')has_video = vcodec != 'none'has_audio = acodec != 'none'height = safe_number(f.get('height', 0))width = safe_number(f.get('width', 0))fps = safe_number(f.get('fps', 0))tbr = safe_number(f.get('tbr', 0))if has_video:  # 只处理包含视频的格式format_notes = []# 添加分辨率标签if height >= 2160:format_notes.append("4K")elif height >= 1440:format_notes.append("2K")# 详细的分辨率信息if height and width:format_notes.append(f"{width:.0f}x{height:.0f}p")# FPS信息if fps > 0:format_notes.append(f"{fps:.0f}fps")# 编码信息if vcodec != 'none':codec_name = {'avc1': 'H.264','vp9': 'VP9','av01': 'AV1'}.get(vcodec.split('.')[0], vcodec)format_notes.append(f"Video: {codec_name}")# 比特率信息if tbr > 0:format_notes.append(f"{tbr:.0f}kbps")# 音频信息if has_audio and acodec != 'none':format_notes.append(f"Audio: {acodec}")format_data = {'format_id': f.get('format_id', ''),'ext': f.get('ext', ''),'filesize': f.get('filesize', 0),'format_note': ' - '.join(format_notes),'vcodec': vcodec,'acodec': acodec,'height': height,'width': width,'fps': fps,'resolution_sort': height * 1000 + fps}if format_data['format_id']:formats.append(format_data)# 按分辨率和FPS排序formats.sort(key=lambda x: x['resolution_sort'], reverse=True)# 移除重复的格式seen_resolutions = set()unique_formats = []for fmt in formats:res_key = f"{fmt['height']:.0f}p-{fmt['fps']:.0f}fps"if res_key not in seen_resolutions:seen_resolutions.add(res_key)unique_formats.append(fmt)return {'title': info.get('title', 'Unknown'),'duration': info.get('duration', 0),'thumbnail': info.get('thumbnail', ''),'formats': unique_formats,'description': info.get('description', ''),'channel': info.get('channel', 'Unknown'),'view_count': info.get('view_count', 0),}except Exception as e:app.logger.error(f"获取视频信息失败: {str(e)}")return {'error': str(e)}def log_progress(d):if d['status'] == 'downloading':try:percent = d.get('_percent_str', 'N/A').strip()speed = d.get('_speed_str', 'N/A').strip()eta = d.get('_eta_str', 'N/A').strip()# 每5%记录一次进度if percent != 'N/A' and float(percent.rstrip('%')) % 5 < 1:app.logger.info(f"下载进度: {percent} | 速度: {speed} | 剩余时间: {eta}")except Exception:passelif d['status'] == 'finished':app.logger.info("下载完成,开始处理文件...")@app.route('/')
def index():"""渲染主页"""return render_template('index.html')@app.route('/api/info', methods=['POST'])
def get_info():"""获取视频信息的API端点"""url = request.json.get('url')if not url:return jsonify({'error': 'URL is required'}), 400info = get_video_info(url)return jsonify(info)@app.route('/api/download', methods=['POST'])
def download_video():"""下载视频的API端点"""url = request.json.get('url')format_id = request.json.get('format_id')if not url or not format_id:app.logger.error('缺少URL或格式ID')return jsonify({'error': 'URL and format_id are required'}), 400try:cleanup_old_files()temp_file = os.path.join(TEMP_DIR, f'download_{time.time_ns()}')app.logger.info(f"创建临时文件: {os.path.basename(temp_file)}")ydl_opts = {'format': f'{format_id}+bestaudio/best','outtmpl': temp_file + '.%(ext)s','quiet': True,'merge_output_format': 'mp4','postprocessors': [{'key': 'FFmpegVideoConvertor','preferedformat': 'mp4',}],'prefer_ffmpeg': True,'keepvideo': False,'progress_hooks': [log_progress],}app.logger.info("开始下载视频...")with yt_dlp.YoutubeDL(ydl_opts) as ydl:info = ydl.extract_info(url, download=True)final_file = ydl.prepare_filename(info)filename = secure_filename(info['title'] + '.mp4')filesize = os.path.getsize(final_file)filesize_mb = filesize / (1024 * 1024)app.logger.info(f"下载完成: {filename} ({filesize_mb:.1f}MB)")download_token = os.urandom(16).hex()DOWNLOADS[download_token] = {'file_path': final_file,'filename': filename,'timestamp': time.time()}return jsonify({'status': 'success','download_token': download_token,'filename': filename})except Exception as e:app.logger.error(f"下载失败: {str(e)}")return jsonify({'error': str(e)}), 500@app.route('/api/get_file/<token>')
def get_file(token):"""获取下载文件的API端点"""if token not in DOWNLOADS:app.logger.error("无效的下载令牌")return 'Invalid or expired download token', 400download_info = DOWNLOADS[token]file_path = download_info['file_path']filename = download_info['filename']if not os.path.exists(file_path):app.logger.error(f"文件未找到: {filename}")return 'File not found', 404try:filesize = os.path.getsize(file_path)filesize_mb = filesize / (1024 * 1024)app.logger.info(f"开始发送: {filename} ({filesize_mb:.1f}MB)")return send_file(file_path,as_attachment=True,download_name=filename,mimetype='video/mp4')except Exception as e:app.logger.error(f"发送文件失败: {str(e)}")return str(e), 500finally:def cleanup():try:if token in DOWNLOADS:os.remove(file_path)del DOWNLOADS[token]app.logger.info(f"临时文件已清理: {filename}")except Exception as e:app.logger.error(f"清理文件失败: {str(e)}")import threadingthreading.Timer(60, cleanup).start()@app.route('/api/logs')
def get_logs():"""获取日志的API端点"""logs = []temp_queue = queue.Queue()try:while not log_queue.empty():log = log_queue.get_nowait()logs.append(log)temp_queue.put(log)while not temp_queue.empty():log_queue.put(temp_queue.get_nowait())return jsonify(sorted(logs, key=lambda x: x['timestamp'], reverse=True))except Exception as e:app.logger.error(f"获取日志失败: {str(e)}")return jsonify([])if __name__ == '__main__':# 确保临时目录存在os.makedirs(TEMP_DIR, exist_ok=True)# 启动时清理旧文件cleanup_old_files()# 运行应用app.run(host='0.0.0.0', port=9012, debug=True)

2. index.html

<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>YouTube Video Downloader</title><link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body><div class="container"><h1>YouTube Video Downloader</h1><div class="input-group"><input type="text" id="url-input" placeholder="Enter YouTube URL"><button id="fetch-info">Get Video Info</button></div><div id="video-info" class="hidden"><div class="info-container"><img id="thumbnail" src="" alt="Video thumbnail"><div class="video-details"><h2 id="video-title"></h2><p id="video-duration"></p></div></div><div class="formats-container"><h3>Available Formats</h3><div id="format-list"></div></div></div><div id="status" class="hidden"></div><!-- 日志显示区域 --><div class="log-container"><div class="log-header"><h3>Operation Logs</h3><button id="clear-logs" title="Clear logs">Clear</button><label class="auto-scroll"><input type="checkbox" id="auto-scroll" checked>Auto-scroll</label></div><div id="log-display"></div></div></div><script src="{{ url_for('static', filename='js/script.js') }}"></script>
</body>
</html>

3. style.css

有了 AI 后, style 产生得太简单

/* static/css/style.css */
body {font-family: Arial, sans-serif;margin: 0;padding: 20px;background-color: #f5f5f5;
}.container {max-width: 800px;margin: 0 auto;background-color: white;padding: 20px;border-radius: 8px;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}h1 {text-align: center;color: #333;margin-bottom: 20px;
}.input-group {display: flex;gap: 10px;margin-bottom: 20px;
}input[type="text"] {flex: 1;padding: 10px;border: 1px solid #ddd;border-radius: 4px;font-size: 16px;
}button {padding: 10px 20px;background-color: #007bff;color: white;border: none;border-radius: 4px;cursor: pointer;font-size: 16px;
}button:hover {background-color: #0056b3;
}.hidden {display: none;
}.info-container {display: flex;gap: 20px;margin-bottom: 20px;padding: 15px;background-color: #f8f9fa;border-radius: 4px;
}#thumbnail {max-width: 200px;border-radius: 4px;
}.video-details {flex: 1;
}.video-details h2 {margin: 0 0 10px 0;color: #333;
}.formats-container {border-top: 1px solid #ddd;padding-top: 20px;
}#format-list {display: grid;gap: 10px;
}.format-item {padding: 10px;background-color: #f8f9fa;border-radius: 4px;display: flex;justify-content: space-between;align-items: center;
}#status {margin: 20px 0;padding: 10px;border-radius: 4px;text-align: center;
}#status.success {background-color: #d4edda;color: #155724;
}#status.error {background-color: #f8d7da;color: #721c24;
}/* 日志容器样式 */
.log-container {margin-top: 20px;border: 1px solid #ddd;border-radius: 4px;background-color: #1e1e1e;
}.log-header {padding: 10px;background-color: #2d2d2d;border-bottom: 1px solid #444;display: flex;align-items: center;gap: 10px;
}.log-header h3 {margin: 0;flex-grow: 1;color: #fff;
}.auto-scroll {display: flex;align-items: center;gap: 5px;font-size: 14px;color: #fff;
}#clear-logs {padding: 5px 10px;background-color: #6c757d;color: white;border: none;border-radius: 4px;cursor: pointer;
}#clear-logs:hover {background-color: #5a6268;
}#log-display {height: 300px;overflow-y: auto;padding: 10px;font-family: 'Consolas', 'Monaco', monospace;font-size: 13px;line-height: 1.4;background-color: #1e1e1e;color: #d4d4d4;
}.log-entry {margin: 2px 0;padding: 2px 5px;border-radius: 2px;white-space: pre-wrap;word-wrap: break-word;
}.log-timestamp {color: #888;margin-right: 8px;font-size: 0.9em;
}.log-info {color: #89d4ff;
}.log-error {color: #ff8989;
}.log-warning {color: #ffd700;
}/* 滚动条样式 */
#log-display::-webkit-scrollbar {width: 8px;
}#log-display::-webkit-scrollbar-track {background: #2d2d2d;
}#log-display::-webkit-scrollbar-thumb {background: #888;border-radius: 4px;
}#log-display::-webkit-scrollbar-thumb:hover {background: #555;
}

4. script.js

// static/js/script.js
document.addEventListener('DOMContentLoaded', function() {const urlInput = document.getElementById('url-input');const fetchButton = document.getElementById('fetch-info');const videoInfo = document.getElementById('video-info');const thumbnail = document.getElementById('thumbnail');const videoTitle = document.getElementById('video-title');const videoDuration = document.getElementById('video-duration');const formatList = document.getElementById('format-list');const status = document.getElementById('status');// 日志系统class Logger {constructor() {this.logDisplay = document.getElementById('log-display');this.autoScrollCheckbox = document.getElementById('auto-scroll');this.clearLogsButton = document.getElementById('clear-logs');this.lastLogTimestamp = null;this.setupEventListeners();}setupEventListeners() {this.clearLogsButton.addEventListener('click', () => this.clearLogs());this.startLogPolling();}formatTimestamp(isoString) {const date = new Date(isoString);return date.toLocaleTimeString('en-US', { hour12: false,hour: '2-digit',minute: '2-digit',second: '2-digit',fractionalSecondDigits: 3});}addLogEntry(entry) {const logEntry = document.createElement('div');logEntry.classList.add('log-entry');if (entry.level === 'error') {logEntry.classList.add('log-error');} else if (entry.level === 'warning') {logEntry.classList.add('log-warning');} else {logEntry.classList.add('log-info');}const timestamp = document.createElement('span');timestamp.classList.add('log-timestamp');timestamp.textContent = this.formatTimestamp(entry.timestamp);const message = document.createElement('span');message.classList.add('log-message');message.textContent = entry.message;logEntry.appendChild(timestamp);logEntry.appendChild(message);this.logDisplay.appendChild(logEntry);if (this.autoScrollCheckbox.checked) {this.scrollToBottom();}}clearLogs() {this.logDisplay.innerHTML = '';this.lastLogTimestamp = null;}scrollToBottom() {this.logDisplay.scrollTop = this.logDisplay.scrollHeight;}async fetchLogs() {try {const response = await fetch('/api/logs');const logs = await response.json();const newLogs = this.lastLogTimestamp ? logs.filter(log => log.timestamp > this.lastLogTimestamp): logs;if (newLogs.length > 0) {newLogs.forEach(log => this.addLogEntry(log));this.lastLogTimestamp = logs[0].timestamp;}} catch (error) {console.error('Failed to fetch logs:', error);}}startLogPolling() {setInterval(() => this.fetchLogs(), 500);}}// 初始化日志系统const logger = new Logger();function formatDuration(seconds) {const hours = Math.floor(seconds / 3600);const minutes = Math.floor((seconds % 3600) / 60);const remainingSeconds = seconds % 60;if (hours > 0) {return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;}return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;}function formatFileSize(bytes) {if (!bytes) return 'Unknown size';const sizes = ['Bytes', 'KB', 'MB', 'GB'];const i = Math.floor(Math.log(bytes) / Math.log(1024));return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;}function showStatus(message, isError = false) {status.textContent = message;status.className = isError ? 'error' : 'success';status.classList.remove('hidden');}async function downloadVideo(url, formatId) {try {logger.addLogEntry({timestamp: new Date().toISOString(),level: 'info',message: `Starting download preparation for format: ${formatId}`});showStatus('Preparing download...');const response = await fetch('/api/download', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ url, format_id: formatId })});const data = await response.json();if (response.ok && data.download_token) {logger.addLogEntry({timestamp: new Date().toISOString(),level: 'success',message: `Download token received: ${data.download_token}`});showStatus('Starting download...');const iframe = document.createElement('iframe');iframe.style.display = 'none';iframe.src = `/api/get_file/${data.download_token}`;iframe.onload = () => {logger.addLogEntry({timestamp: new Date().toISOString(),level: 'success',message: `Download started for: ${data.filename}`});showStatus('Download started! Check your browser downloads.');setTimeout(() => document.body.removeChild(iframe), 5000);};iframe.onerror = () => {logger.addLogEntry({timestamp: new Date().toISOString(),level: 'error',message: 'Download failed to start'});showStatus('Download failed. Please try again.', true);document.body.removeChild(iframe);};document.body.appendChild(iframe);} else {const errorMessage = data.error || 'Download failed';logger.addLogEntry({timestamp: new Date().toISOString(),level: 'error',message: `Download failed: ${errorMessage}`});showStatus(errorMessage, true);}} catch (error) {logger.addLogEntry({timestamp: new Date().toISOString(),level: 'error',message: `Network error: ${error.message}`});showStatus('Network error occurred', true);console.error(error);}}fetchButton.addEventListener('click', async () => {const url = urlInput.value.trim();if (!url) {showStatus('Please enter a valid URL', true);return;}showStatus('Fetching video information...');try {const response = await fetch('/api/info', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ url })});const data = await response.json();if (response.ok) {thumbnail.src = data.thumbnail;videoTitle.textContent = data.title;videoDuration.textContent = formatDuration(data.duration);formatList.innerHTML = data.formats.filter(format => format.format_id && format.ext).map(format => `<div class="format-item"><span>${format.format_note} (${format.ext}) - ${formatFileSize(format.filesize)}</span><button onclick="downloadVideo('${url}', '${format.format_id}')">Download</button></div>`).join('');videoInfo.classList.remove('hidden');status.classList.add('hidden');logger.addLogEntry({timestamp: new Date().toISOString(),level: 'info',message: `Video information retrieved: ${data.title}`});} else {showStatus(data.error || 'Failed to fetch video info', true);logger.addLogEntry({timestamp: new Date().toISOString(),level: 'error',message: `Failed to fetch video info: ${data.error || 'Unknown error'}`});}} catch (error) {showStatus('Network error occurred', true);logger.addLogEntry({timestamp: new Date().toISOString(),level: 'error',message: `Network error: ${error.message}`});}});window.downloadVideo = downloadVideo;// 支持回车键触发获取视频信息urlInput.addEventListener('keypress', (e) => {if (e.key === 'Enter') {fetchButton.click();}});});

以上文件放到相应目录,库文件参考 requirements.txt 即可。

Docker 部署

1. Dockerfile

FROM python:3.11-slimWORKDIR /appRUN apt-get update && \apt-get install -y --no-install-recommends \ffmpeg \&& rm -rf /var/lib/apt/lists/*COPY app.py ./
COPY static/css/style.css ./static/css/
COPY static/js/script.js ./static/js/
COPY templates/index.html ./templates/RUN pip install --no-cache-dir \flask \yt-dlp \werkzeugRUN mkdir -p /app/temp_downloads && \chmod 777 /app/temp_downloadsENV FLASK_APP=app.py
ENV PYTHONUNBUFFERED=1
ENV FLASK_RUN_HOST=0.0.0.0
ENV FLASK_RUN_PORT=9012EXPOSE 9012CMD ["python", "-c", "from app import app; app.run(host='0.0.0.0', port=9012)"]

2. requirements.txt

flask
Werkzeug==3.0.1
yt-dlp==2024.3.10
gunicorn==21.2.0

如果你使用这个 .txt, 可以去掉版本号。我指定版本号,是因数我的 NAS 上面的 wheel files 有重复的。

3. 创建 Image 与 Container

# docker build -t yt-dlp .
# docker run -d -p 9012:9012 --name yt-dlp_container yt-dlp我使用了与 Github 上面项目的相同名字,只是为了方便,字少。

注:在 docker 命令中没有 加入 --restart always, 要编辑一下容器自己添加。

总结:

yt-dlp 是一个功能超强的工具,可以用 cookie file获取身份认证来下载视频,或通过 Mozila 浏览器直接获得 cookie 内容(只是说明上这么说,我没试过)。 Douyin 有 bug 不能下载 , 其它网站没有试。

我有订阅 youtube ,这个工具只是娱乐,或下载 民国 及以前的,其版权已经放弃的影像内容。

请尊重版权

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/465804.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Sqli-Labs

目录 解题思路 题目设计原理 总结 解题思路 什么&#xff1f;sqli-labs&#xff1f;让我看看。还真是。想起了当初刚学被支配的恐惧。 悄咪咪点开第一关看看能不能秒了。测试闭合老样子&#xff0c;单引号闭合&#xff0c;双引号等都成功。这里 and 11 和 # 都不能通过检测&…

【基于Zynq FPGA对雷龙SD NAND的测试】

一、SD NAND 特征 1.1 SD 卡简介 雷龙的 SD NAND 有很多型号&#xff0c;在测试中使用的是 CSNP4GCR01-AMW 与 CSNP32GCR01-AOW。芯片是基于 NAND FLASH 和 SD 控制器实现的 SD 卡。具有强大的坏块管理和纠错功能&#xff0c;并且在意外掉电的情况下同样能保证数据的安全。 …

【NOIP提高组】引水入城

【NOIP提高组】引水入城 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 在一个遥远的国度&#xff0c;一侧是风景秀美的湖泊&#xff0c;另一侧则是漫无边际的沙漠。该国的行政 区划十分特殊&#xff0c;刚好构成一个N行M列的矩形&#xff…

鸿蒙开发:arkts 如何读取json数据

为了支持ArkTS语言的开发&#xff0c;华为提供了完善的工具链&#xff0c;包括代码编辑器、编译器、调试器、测试工具等。开发者可以使用这些工具进行ArkTS应用的开发、调试和测试。同时&#xff0c;华为还提供了DevEco Studio这一一站式的开发平台&#xff0c;为运行在Harmony…

OpenCV视觉分析之目标跟踪(11)计算两个图像之间的最佳变换矩阵函数findTransformECC的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 根据 ECC 标准 78找到两幅图像之间的几何变换&#xff08;warp&#xff09;。 该函数根据 ECC 标准 ([78]) 估计最优变换&#xff08;warpMatri…

【2024最新版Kotlin教程】Kotlin第一行代码系列第五课-类继承,抽象类,接口

【2024最新版Kotlin教程】Kotlin第一行代码系列第五课-类继承&#xff0c;抽象类&#xff0c;接口 为什么要有继承呢&#xff0c;现实中也是有继承的&#xff0c;对吧&#xff0c;你继承你爸的遗产&#xff0c;比如你爸建好了一个房子&#xff0c;儿子继承爸&#xff0c;就得了…

iOS用rime且导入自制输入方案

iPhone 16 的 cantonese 只能打传统汉字&#xff0c;没有繁简转换&#xff0c;m d sh d。考虑用「仓」输入法 [1] 使用 Rime 打字&#xff0c;且希望导入自制方案 [2]。 仓输入法有几种导入方案的方法&#xff0c;见 [3]&#xff0c;此处记录 wifi 上传法。准备工作&#xff1…

基于Zynq FPGA的雷龙SD NAND存储芯片性能测试

文章目录 前言一、SD NAND特征1.1 SD卡简介1.2 SD卡Block图 二、SD卡样片三、Zynq测试平台搭建3.1 测试流程3.2 SOC搭建 四、软件搭建五、测试结果六、总结 前言 随着嵌入式系统和物联网设备的快速发展&#xff0c;高效可靠的存储解决方案变得越来越重要。雷龙发展推出的SD NA…

【动态规划 数学】2745. 构造最长的新字符串|1607

本文涉及知识点 C动态规划 数学 LeetCode2745. 构造最长的新字符串 给你三个整数 x &#xff0c;y 和 z 。 这三个整数表示你有 x 个 “AA” 字符串&#xff0c;y 个 “BB” 字符串&#xff0c;和 z 个 “AB” 字符串。你需要选择这些字符串中的部分字符串&#xff08;可以全…

【Linux驱动开发】timer库下的jiffies时间戳和延时驱动编写

【Linux驱动开发】timer库下的jiffies时间戳和延时驱动编写 gitee地址&#xff1a; https://gitee.com/Mike_Zhou_Admin/Linux_Driver_Timestamp_Driver/更新以gitee为准 文章目录 timer库时间戳函数延时函数驱动代码应用测试附录&#xff1a;嵌入式Linux驱动开发基本步骤开发…

了解云计算工作负载保护的重要性及必要性

云计算de小白 云计算技术的快速发展使数据和应用程序安全成为一种关键需求&#xff0c;而不仅仅是一种偏好。随着越来越多的客户公司将业务迁移到云端&#xff0c;保护他们的云工作负载&#xff08;指所有部署的应用程序和服务&#xff09;变得越来越重要。云工作负载保护&…

C语言 循环高级

时间&#xff1a;2024.11.6 一、学习内容 1、无限循环 无限循环&#xff1a;循环永远停不下来 注意点&#xff1a;无限循环因为永远停不下来&#xff0c;所以下面不能再写其他的代码了 2、break 跳转控制语句&#xff1a; 在循环的过程中&#xff0c;跳到其他语句上执行 #…

易语言模拟真人动态生成鼠标滑动路径

一.简介 鼠标轨迹算法是一种模拟人类鼠标操作的程序&#xff0c;它能够模拟出自然而真实的鼠标移动路径。 鼠标轨迹算法的底层实现采用C/C语言&#xff0c;原因在于C/C提供了高性能的执行能力和直接访问操作系统底层资源的能力。 鼠标轨迹算法具有以下优势&#xff1a; 模拟…

CSS学习之Grid网格布局基本概念、容器属性

网格布局 网格布局&#xff08;Grid&#xff09;是将网页划分成一个个网格单元&#xff0c;可任意组合不同的网格&#xff0c;轻松实现各种布局效果&#xff0c;也是目前CSS中最强大布局方案&#xff0c;比Flex更强大。 基本概念 容器和项目 当一个 HTML 元素将 display 属性…

聊一聊Elasticsearch的索引的分片分配机制

1、什么是分片分配 分片分配是由ES主节点将索引分片移动到ES集群中各个节点上的过程。 该过程尽量保证&#xff0c;同一个索引的分片尽量分配到更多的节点上&#xff0c;以此来达到读写索引的时候可以利用更多硬件资源的效果。 在分配过程当中&#xff0c;也不能将某个主分片…

springboot的增删改查商城小实践(b to c)

首先准备一张表&#xff0c;根据业务去设计表 订单编号是参与业务的&#xff0c;他那订单编号里面是有特殊意义的&#xff0c;比如说像什么一些年月日什么的&#xff0c;一些用户的ID都在那编号里面呢&#xff1f;不能拿这种东西当主件啊 根据数据量去决定数据类型 价格需要注意…

Ubuntu 安装 RTL8811cu 网卡驱动

一、支持的网卡 RTL8811AU、RTL8811CU、RTL8821AU、RTL8821CU 二、下载驱动 github&#xff1a;https://github.com/brektrou/rtl8821CU 直接下载zip源码即可 三、安装驱动 sudo su -i #切换到root用户 apt-get update #更新安装源 apt-get install -y dkms …

解锁炎症和肿瘤免疫治疗新靶点:TREM1&TREM2

前 言 TREM家族属于细胞表面受体&#xff0c;介导调控炎症反应&#xff0c;现已成为癌症、神经退行性疾病以及炎症性疾病等多种疾病最有潜力的药物靶点。截至2023年6月&#xff0c;有5项FDA注册的临床前或临床试验正在进行中&#xff0c;有3项是TREM2在阿尔茨海默症&#xff…

【Unity】Unity拖拽在Android设备有延迟和卡顿问题的解决

一、介绍 在制作Block类游戏时&#xff0c;其核心的逻辑就是拖拽方块放入到地图中&#xff0c;这里最先想到的就是Unity的拖拽接口IDragHandler,然后通过 IPointerDownHandler, IPointerUpHandler 这两个接口判断按下和松手&#xff0c;具体的实现逻辑就是下面 public void On…

Postman断言与依赖接口测试详解!

在接口测试中&#xff0c;断言是不可或缺的一环。它不仅能够自动判断业务逻辑的正确性&#xff0c;还能确保接口的实际功能实现符合预期。Postman作为一款强大的接口测试工具&#xff0c;不仅支持发送HTTP请求和接收响应&#xff0c;还提供了丰富的断言功能&#xff0c;帮助测试…