首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

< Project-30.GitHub_Apps/3.cobalt > imputnet/cobalt 用来下载视频的,以前它自己有个web, 现在没了 自己做个web套套 占用 9016 商品

  • 25-02-19 08:41
  • 3987
  • 13069
blog.csdn.net

前言:

GitHub 上的一个用于下载视频的项目: Cobalt 链接:https://github.com/imputnet/cobalt

以前有个 web 界面,后来升级没了。 刚才用 yt-dlp 想下载个视频又报错,看了一下,pip 升级后 docker 能用了,但 PC 上可能用了 VPN 报的要验证不是 bot ...

花点儿功夫,顺手把这个去年想做的界面搞定了。

简约界面

使用英文,说是因打字时总会有错字...

功能介绍:

cobalt:

cobalt helps you save anything from your favorite websites: video, audio, photos or gifs. just paste the link and you’re ready to rock!

no ads, trackers, paywalls, or other nonsense. just a convenient web app that works anywhere, whenever you need it.

https://cobalt.tools/about/general

抢劫/盗窃 “流行”网站的视频、音频。  (有买 Youtube Premium ,下载 Youtube 视频只是对这几个GitHub感兴趣,视频不会保存,版权是要尊重的。)

Cobalt Downloader  Web:

  • 下载主流网站的视频、音频
  • youtube 可以选择码率,为了省流量,默认选择是 480p
  • 只是调用 Cobalt API
  • 使用 docker-compose.yml 在 docker 部署,快~
  • 使用 flask 占用 9016 端口
  • js html css 是独立文件
  • Cobalt API 占用 9000 端口

Cobalt + Web 完整结构

1. 在 docker 上部署 Cobalt Instance 

GitHub 参考:https://github.com/imputnet/cobalt/blob/main/docs/run-an-instance.md

因为 Cobalt 用的是 docker compose up -d 来安装,没有 image 占地儿。Web UI 我也用这方法。

简单步骤:

        a. NAS 上给 Cobalt 项目做个目录,例:
[/share/Multimedia/2024-MyProgramFiles/30.GitHub_Apps/3.cobalt-main/cobalt] #
        b. 把 docker-compose.yml 放到这个目录里

GitHub 上的 sample file:  https://github.com/imputnet/cobalt/blob/main/docs/examples/docker-compose.example.yml

稍微学习了一下文件的格式,修改后如下:

如果你是第一次部署,建议使用我的这个 docker-compose.yml 文件,成功后再继续修改。

c. 完整的 docker-compose.yml:
  1. services:
  2. cobalt-api:
  3. image: ghcr.io/imputnet/cobalt:10
  4. init: true
  5. read_only: true
  6. restart: unless-stopped
  7. container_name: cobalt-api
  8. ports:
  9. - 9000:9000/tcp
  10. environment:
  11. API_URL: "http://192.168.1.8:9000/"
  12. # 添加以下环境变量来启用格式列表
  13. DURATION_LIMIT: "10800" # 3小时视频限制
  14. RATELIMIT_MAX: "30" # 每个时间窗口允许的最大请求数
  15. RATELIMIT_WINDOW: "60" # 时间窗口(秒)
  16. TUNNEL_LIFESPAN: "90" # 下载链接有效期(秒)
  17. labels:
  18. - com.centurylinklabs.watchtower.scope=cobalt
  19. watchtower:
  20. image: ghcr.io/containrrr/watchtower
  21. restart: unless-stopped
  22. command: --cleanup --scope cobalt --interval 900 --include-restarting
  23. volumes:
  24. - /var/run/docker.sock:/var/run/docker.sock
d. 说明:

以下你可以自行修改:

  • 我没有使用 API KEY
  • cobalt-api 端口占用 9000
  • API_URL 使用的是我自己的 NAS IP 地址
e. 在 docker 上部署

进入 Cobalt 目录,运行: docker compose up -d

 [/share/Multimedia/2024-MyProgramFiles/30.GitHub_Apps/3.cobalt-main/cobalt] # docker compose up -d

完成后,会看到:

2. 部署 Web

a. 目录结构

  1. cobalt-main/app/
  2. │
  3. ├── app.py # Main application file
  4. ├── requirements.txt # dependencies
  5. ├── docker-compose.yml # Docker configuration
  6. ├── Dockerfile # Docker image build instructions
  7. ├── key.pem # SSL/TLS private key
  8. ├── cert.pem # SSL/TLS certificate
  9. ├── templates/ # HTML directory
  10. │ ├── 404.html # Not Found errors
  11. │ ├── 500.html # Internal errors
  12. │ ├── base.html # Base
  13. │ └── index.html # home page
  14. │
  15. └── static/
  16. ├── download.js # JavaScript
  17. ├── favicon.ico
  18. └── css/
  19. └── style.css # CSS

b. 完整代码文件

1) app.py

  1. from flask import Flask, request, jsonify, render_template, send_from_directory
  2. import requests
  3. import ssl
  4. import os
  5. import logging
  6. from logging.handlers import RotatingFileHandler
  7. from dotenv import load_dotenv
  8. # 加载环境变量
  9. load_dotenv()
  10. # Configure logging
  11. logging.basicConfig(level=logging.INFO)
  12. logger = logging.getLogger(__name__)
  13. handler = RotatingFileHandler('logs/app.log', maxBytes=10000000, backupCount=5)
  14. handler.setFormatter(logging.Formatter(
  15. '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
  16. ))
  17. logger.addHandler(handler)
  18. # Flask app configuration
  19. app = Flask(__name__,
  20. template_folder='templates',
  21. static_folder='static')
  22. # 生产环境设置
  23. app.config['ENV'] = os.getenv('FLASK_ENV', 'production')
  24. app.config['DEBUG'] = os.getenv('FLASK_DEBUG', '0') == '1'
  25. # Cobalt API configuration
  26. COBALT_API_URL = os.getenv('COBALT_API_URL', 'http://192.168.1.8:9000')
  27. API_KEY = os.getenv('COBALT_API_KEY')
  28. # Security headers
  29. @app.after_request
  30. def add_security_headers(response):
  31. response.headers['X-Content-Type-Options'] = 'nosniff'
  32. response.headers['X-Frame-Options'] = 'DENY'
  33. response.headers['X-XSS-Protection'] = '1; mode=block'
  34. response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
  35. return response
  36. @app.route('/', methods=['GET'])
  37. def index():
  38. """Render the main page."""
  39. try:
  40. logger.info("Attempting to render index.html")
  41. return render_template('index.html')
  42. except Exception as e:
  43. logger.error(f"Error rendering template: {str(e)}")
  44. return render_template('500.html'), 500
  45. @app.route('/static/')
  46. def serve_static(path):
  47. """Serve static files."""
  48. return send_from_directory('static', path)
  49. @app.route('/api/download', methods=['POST'])
  50. def download():
  51. """Handle download requests by proxying them to Cobalt API."""
  52. try:
  53. # 1. Validate request data
  54. data = request.get_json()
  55. if not data or 'url' not in data:
  56. logger.warning("Missing URL in request")
  57. return jsonify({
  58. 'status': 'error',
  59. 'message': 'URL is required'
  60. }), 400
  61. # 2. Log request info
  62. logger.info(f"Processing download request for URL: {data['url']}")
  63. logger.info(f"Using Cobalt API URL: {COBALT_API_URL}")
  64. # 3. Build base payload with mandatory parameters
  65. payload = {
  66. 'url': data['url'],
  67. 'filenameStyle': 'pretty'
  68. }
  69. # 4. Add mode-specific parameters
  70. mode = data.get('mode', 'auto')
  71. payload['downloadMode'] = mode
  72. # Handle video modes
  73. if mode in ['auto', 'mute']:
  74. payload.update({
  75. 'videoQuality': data.get('videoQuality', 'max'),
  76. 'youtubeVideoCodec': 'h264'
  77. })
  78. # Handle audio mode
  79. elif mode == 'audio':
  80. payload.update({
  81. 'audioFormat': data.get('audioFormat', 'mp3'),
  82. 'audioBitrate': data.get('audioBitrate', '320')
  83. })
  84. # 5. Add optional service-specific parameters
  85. if data.get('tiktokFullAudio'):
  86. payload['tiktokFullAudio'] = True
  87. if data.get('tiktokH265') is not None:
  88. payload['tiktokH265'] = data['tiktokH265']
  89. if data.get('twitterGif') is not None:
  90. payload['twitterGif'] = data['twitterGif']
  91. if data.get('youtubeDubLang'):
  92. payload['youtubeDubLang'] = data['youtubeDubLang']
  93. if data.get('youtubeHLS') is not None:
  94. payload['youtubeHLS'] = data['youtubeHLS']
  95. # 6. Add optional global parameters
  96. if data.get('alwaysProxy') is not None:
  97. payload['alwaysProxy'] = data['alwaysProxy']
  98. if data.get('disableMetadata') is not None:
  99. payload['disableMetadata'] = data['disableMetadata']
  100. # 7. Prepare request headers
  101. headers = {
  102. 'Content-Type': 'application/json',
  103. 'Accept': 'application/json'
  104. }
  105. # 8. Add API authentication if configured
  106. if API_KEY:
  107. headers['Authorization'] = f'Api-Key {API_KEY}'
  108. logger.info("Using API key authentication")
  109. # 9. Send request to Cobalt API
  110. logger.info(f"Sending request to Cobalt API with payload: {payload}")
  111. response = requests.post(
  112. f"{COBALT_API_URL}/",
  113. headers=headers,
  114. json=payload,
  115. timeout=30
  116. )
  117. # 10. Log response info
  118. logger.info(f"Cobalt API response status: {response.status_code}")
  119. logger.info(f"Cobalt API response headers: {dict(response.headers)}")
  120. # 11. Handle successful response
  121. if response.status_code == 200:
  122. logger.info("Successfully received response from Cobalt API")
  123. cobalt_data = response.json()
  124. logger.info(f"Cobalt API response data: {cobalt_data}")
  125. return jsonify(cobalt_data)
  126. # 12. Handle error response
  127. logger.error(f"Cobalt API error response: {response.text}")
  128. return jsonify({
  129. 'status': 'error',
  130. 'message': f'API Error: {response.status_code}'
  131. }), response.status_code
  132. except requests.RequestException as e:
  133. # 13. Handle network errors
  134. logger.error(f"Network error details: {str(e)}")
  135. return jsonify({
  136. 'status': 'error',
  137. 'message': f'Network error: {str(e)}'
  138. }), 500
  139. except Exception as e:
  140. # 14. Handle unexpected errors
  141. logger.error(f"Unexpected error details: {str(e)}")
  142. return jsonify({
  143. 'status': 'error',
  144. 'message': str(e)
  145. }), 500
  146. @app.errorhandler(404)
  147. def not_found_error(error):
  148. """Handle 404 errors."""
  149. logger.warning(f"404 error: {request.url}")
  150. return render_template('404.html'), 404
  151. @app.errorhandler(500)
  152. def internal_error(error):
  153. """Handle 500 errors."""
  154. logger.error(f"500 error: {error}")
  155. return render_template('500.html'), 500
  156. if __name__ == '__main__':
  157. # Ensure logs directory exists
  158. os.makedirs('logs', exist_ok=True)
  159. logger.info("Starting Flask application...")
  160. logger.info(f"Working directory: {os.getcwd()}")
  161. logger.info(f"Directory contents: {os.listdir()}")
  162. logger.info(f"Templates directory contents: {os.listdir('templates') if os.path.exists('templates') else 'No templates directory'}")
  163. # SSL configuration
  164. context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
  165. try:
  166. context.load_cert_chain('cert.pem', 'key.pem')
  167. logger.info("Successfully loaded SSL certificates")
  168. app.run(host='0.0.0.0', port=9016, ssl_context=context)
  169. except Exception as e:
  170. logger.error(f"SSL Error: {e}")
  171. logger.info("Falling back to HTTP...")
  172. app.run(host='0.0.0.0', port=9016)

2) requirements.txt

  1. flask
  2. requests
  3. python-dotenv
  4. gunicorn

建议使用版本号,别学我。

3) docker-compose.yml

  1. services:
  2. flask-app:
  3. build: .
  4. container_name: flask-cobalt
  5. network_mode: "host" # 使用主机网络模式
  6. environment:
  7. - COBALT_API_URL=http://192.168.1.8:9000
  8. - FLASK_ENV=production
  9. - FLASK_DEBUG=1
  10. volumes:
  11. - ./static:/app/static:ro
  12. - ./templates:/app/templates:ro
  13. - ./cert.pem:/app/cert.pem:ro
  14. - ./key.pem:/app/key.pem:ro
  15. - ./logs:/app/logs
  16. restart: unless-stopped

COBALT_API_URL=http://192.168.1.8:9000 要与 cobalt docker-compose.yml 中的 environment:
API_URL: 一至

4) Dockerfile

  1. FROM python:3.12-slim
  2. WORKDIR /app
  3. ENV PYTHONDONTWRITEBYTECODE=1
  4. ENV PYTHONUNBUFFERED=1
  5. COPY requirements.txt .
  6. RUN pip install --no-cache-dir -r requirements.txt
  7. COPY . .
  8. RUN chmod 600 key.pem && chmod 644 cert.pem
  9. CMD ["gunicorn", "--certfile=cert.pem", "--keyfile=key.pem", "--bind", "0.0.0.0:9016", "app:app"]

你需要自己准备 SSL 要用的 两个文件 cert.pem 与 key.pem 支持 SSH 连接。 如果没有,会运行在 http 下。

5) 404.html

  1. {% extends "base.html" %}
  2. {% block content %}
  3. <div class="error-container">
  4. <h1>404 - Page Not Found</h1>
  5. <p>The page you're looking for doesn't exist.</p>
  6. <a href="/" class="back-link">Return to Home</a>
  7. </div>
  8. {% endblock content %}

6) 500.html 

  1. {% extends "base.html" %}
  2. {% block content %}
  3. <div class="error-container">
  4. <h1>500 - Server Error</h1>
  5. <p>Something went wrong on our end. Please try again later.</p>
  6. <a href="/" class="back-link">Return to Home</a>
  7. </div>
  8. {% endblock content %}

7) base.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>{% block title %}Cobalt Downloader{% endblock title %}</title>
  7. <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
  8. <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
  9. <script src="{{ url_for('static', filename='download.js') }}" defer></script>
  10. </head>
  11. <body>
  12. <header>
  13. <nav>
  14. <div class="container">
  15. <h1>Cobalt Downloader</h1>
  16. </div>
  17. </nav>
  18. </header>
  19. <main class="container">
  20. {% block content %}{% endblock content %}
  21. </main>
  22. <footer>
  23. <div class="container">
  24. <p>&copy; 2024 Cobalt Downloader</p>
  25. </div>
  26. </footer>
  27. </body>
  28. </html>

8) index.html

  1. {% extends "base.html" %}
  2. {% block content %}
  3. <style>
  4. .format-list {
  5. margin-top: 1rem;
  6. display: flex;
  7. flex-direction: column;
  8. gap: 0.5rem;
  9. }
  10. .format-button {
  11. background-color: var(--primary-color);
  12. color: white;
  13. border: none;
  14. padding: 0.5rem 1rem;
  15. border-radius: 4px;
  16. cursor: pointer;
  17. font-size: 0.9rem;
  18. transition: background-color 0.2s;
  19. }
  20. .format-button:hover {
  21. background-color: var(--secondary-color);
  22. }
  23. .option-group {
  24. background: #f8f9fa;
  25. padding: 1rem;
  26. border-radius: 4px;
  27. margin-bottom: 1rem;
  28. }
  29. .option-group h3 {
  30. font-size: 1rem;
  31. margin-bottom: 0.5rem;
  32. color: #666;
  33. }
  34. .option-description {
  35. font-size: 0.85rem;
  36. color: #666;
  37. margin-top: 0.25rem;
  38. }
  39. .select-wrapper {
  40. position: relative;
  41. margin-bottom: 1rem;
  42. }
  43. .select-wrapper::after {
  44. content: "▼";
  45. font-size: 0.8rem;
  46. position: absolute;
  47. right: 1rem;
  48. top: 50%;
  49. transform: translateY(-50%);
  50. pointer-events: none;
  51. color: #666;
  52. }
  53. </style>
  54. <div class="download-form">
  55. <!-- URL Input -->
  56. <div class="form-group">
  57. <input type="url" id="url" placeholder="Enter video/audio URL" required>
  58. </div>
  59. <!-- Video Options -->
  60. <div class="option-group">
  61. <h3>Download Options</h3>
  62. <!-- Download Mode -->
  63. <div class="select-wrapper">
  64. <select id="mode" class="form-select">
  65. <option value="auto">Auto (Video with Audio)</option>
  66. <option value="audio">Audio Only</option>
  67. <option value="mute">Video Only (No Audio)</option>
  68. </select>
  69. <p class="option-description">Choose how you want to download the content</p>
  70. </div>
  71. <!-- Video Quality -->
  72. <div class="select-wrapper">
  73. <select id="videoQuality" class="form-select">
  74. <option value="max">Best Quality</option>
  75. <option value="2160">4K (2160p)</option>
  76. <option value="1440">2K (1440p)</option>
  77. <option value="1080">1080p</option>
  78. <option value="720">720p</option>
  79. <option value="480" selected>Default 480p</option> <!-- also need to modify download.js paylod value -->>
  80. <option value="360">360p</option>
  81. </select>
  82. <p class="option-description">Select video quality (for video downloads)</p>
  83. </div>
  84. <!-- Audio Quality (for audio mode) -->
  85. <div class="select-wrapper">
  86. <select id="audioBitrate" class="form-select">
  87. <option value="320">320 kbps</option>
  88. <option value="256">256 kbps</option>
  89. <option value="128">128 kbps</option>
  90. <option value="96">96 kbps</option>
  91. <option value="64">64 kbps</option>
  92. </select>
  93. <p class="option-description">Select audio quality (for audio-only downloads)</p>
  94. </div>
  95. </div>
  96. <!-- Download Button -->
  97. <button onclick="checkFormats()" class="download-btn">Download</button>
  98. <!-- Result Area -->
  99. <div id="result" class="result" style="display:none;">
  100. <div class="result-content">
  101. <p id="status"></p>
  102. <div id="format-list"></div>
  103. <a id="download-link" href="#" target="_blank" style="display:none;">Download File</a>
  104. </div>
  105. </div>
  106. </div>
  107. {% endblock content %}

9) download.js

  1. // Initialize UI state
  2. document.addEventListener('DOMContentLoaded', function() {
  3. resetResults();
  4. });
  5. // Main format check function
  6. // Main format check function
  7. function checkFormats() {
  8. const url = document.getElementById('url').value;
  9. if (!validateUrl(url)) {
  10. showError('Please enter a valid URL');
  11. return;
  12. }
  13. const payload = {
  14. url: url,
  15. filenameStyle: "pretty",
  16. downloadMode: document.getElementById('mode').value,
  17. videoQuality: document.getElementById('videoQuality').value || '480', // 添加质量选择
  18. youtubeVideoCodec: 'h264'
  19. };
  20. const button = document.querySelector('.download-btn');
  21. const status = document.getElementById('status');
  22. const result = document.getElementById('result');
  23. button.disabled = true;
  24. status.textContent = 'Preparing download...';
  25. result.style.display = 'block';
  26. // 添加 TikTok 选项
  27. if (document.getElementById('tiktokFullAudio').checked) {
  28. payload.tiktokFullAudio = true;
  29. }
  30. // 添加 Twitter 选项
  31. if (document.getElementById('twitterGif').checked) {
  32. payload.twitterGif = true;
  33. }
  34. fetch('/api/download', {
  35. method: 'POST',
  36. headers: {
  37. 'Content-Type': 'application/json',
  38. 'Accept': 'application/json'
  39. },
  40. body: JSON.stringify(payload)
  41. })
  42. .then(response => response.json())
  43. .then(data => {
  44. button.disabled = false;
  45. if (data.status === 'tunnel' || data.status === 'redirect') {
  46. handleDirectDownload(data);
  47. } else {
  48. showError('Unexpected response from server');
  49. }
  50. })
  51. .catch(error => {
  52. button.disabled = false;
  53. showError(error.message);
  54. });
  55. }
  56. // Handle direct download response
  57. function handleDirectDownload(data) {
  58. const status = document.getElementById('status');
  59. const downloadLink = document.getElementById('download-link');
  60. status.textContent = 'Ready to download!';
  61. downloadLink.href = data.url;
  62. downloadLink.textContent = data.filename || 'Download File';
  63. downloadLink.style.display = 'block';
  64. }
  65. // Handle format picker response
  66. function handleFormatPicker(data) {
  67. const status = document.getElementById('status');
  68. const formatList = document.getElementById('format-list');
  69. status.textContent = 'Available formats:';
  70. formatList.innerHTML = '';
  71. const formatDiv = document.createElement('div');
  72. formatDiv.className = 'format-list';
  73. // Sort formats by quality (highest first)
  74. if (data.picker && Array.isArray(data.picker)) {
  75. data.picker.sort((a, b) => {
  76. // Extract numeric value from quality (e.g., "1080p" -> 1080)
  77. const getQualityNumber = (quality) => {
  78. if (!quality) return 0;
  79. const match = quality.match(/(\d+)/);
  80. return match ? parseInt(match[1]) : 0;
  81. };
  82. return getQualityNumber(b.quality) - getQualityNumber(a.quality);
  83. });
  84. // Create buttons for each format
  85. data.picker.forEach((item, index) => {
  86. const button = document.createElement('button');
  87. button.className = 'format-button';
  88. // Build detailed format description
  89. let description = [];
  90. // Add quality info
  91. if (item.quality) {
  92. description.push(item.quality);
  93. }
  94. // Add type (VIDEO/AUDIO)
  95. if (item.type) {
  96. description.push(item.type.toUpperCase());
  97. }
  98. // Add codec info
  99. if (item.codec) {
  100. description.push(`[${item.codec}]`);
  101. }
  102. // Add bitrate if available
  103. if (item.bitrate) {
  104. description.push(`${item.bitrate}kbps`);
  105. }
  106. // Add filesize if available
  107. if (item.filesize) {
  108. const size = formatFileSize(item.filesize);
  109. description.push(size);
  110. }
  111. button.textContent = description.join(' - ');
  112. button.onclick = () => startDownload(item.url, data.filename || `file_${index + 1}`);
  113. // Add hover tooltip with full details
  114. button.title = `Quality: ${item.quality || 'N/A'}\nCodec: ${item.codec || 'N/A'}\nBitrate: ${item.bitrate || 'N/A'}kbps`;
  115. formatDiv.appendChild(button);
  116. });
  117. }
  118. formatList.appendChild(formatDiv);
  119. }
  120. function handleAudioFormatPicker(data, mode) {
  121. const status = document.getElementById('status');
  122. const formatList = document.getElementById('format-list');
  123. status.textContent = 'Available audio formats:';
  124. formatList.innerHTML = '';
  125. const formatDiv = document.createElement('div');
  126. formatDiv.className = 'format-list';
  127. // Sort formats by bitrate (highest first)
  128. if (data.picker && Array.isArray(data.picker)) {
  129. data.picker
  130. .filter(item => mode === 'audio' ? item.type?.toLowerCase() === 'audio' : true)
  131. .sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))
  132. .forEach((item, index) => {
  133. const button = document.createElement('button');
  134. button.className = 'format-button';
  135. // Build format description
  136. let description = [];
  137. if (item.codec) {
  138. description.push(item.codec.toUpperCase());
  139. }
  140. if (item.bitrate) {
  141. description.push(`${item.bitrate}kbps`);
  142. }
  143. if (item.filesize) {
  144. const size = formatFileSize(item.filesize);
  145. description.push(size);
  146. }
  147. button.textContent = description.join(' - ');
  148. button.onclick = () => startDownload(item.url, data.filename || `file_${index + 1}`);
  149. // Add hover tooltip
  150. button.title = `Format: ${item.codec || 'N/A'}\nBitrate: ${item.bitrate || 'N/A'}kbps`;
  151. formatDiv.appendChild(button);
  152. });
  153. }
  154. formatList.appendChild(formatDiv);
  155. // If no formats were found, show message
  156. if (!formatDiv.children.length) {
  157. status.textContent = 'No audio formats available';
  158. }
  159. }
  160. // Utility function to format file size
  161. function formatFileSize(bytes) {
  162. if (!bytes) return '';
  163. const units = ['B', 'KB', 'MB', 'GB'];
  164. let size = bytes;
  165. let unitIndex = 0;
  166. while (size >= 1024 && unitIndex < units.length - 1) {
  167. size /= 1024;
  168. unitIndex++;
  169. }
  170. return `${size.toFixed(1)} ${units[unitIndex]}`;
  171. }
  172. // Start download for selected format
  173. function startDownload(url, filename) {
  174. const downloadLink = document.getElementById('download-link');
  175. downloadLink.href = url;
  176. downloadLink.textContent = filename;
  177. downloadLink.style.display = 'block';
  178. document.getElementById('status').textContent = 'Ready to download!';
  179. downloadLink.click(); // Auto-start download
  180. }
  181. // Reset UI elements
  182. function resetResults() {
  183. const result = document.getElementById('result');
  184. const status = document.getElementById('status');
  185. const formatList = document.getElementById('format-list');
  186. const downloadLink = document.getElementById('download-link');
  187. result.style.display = 'none';
  188. status.textContent = '';
  189. formatList.innerHTML = '';
  190. downloadLink.style.display = 'none';
  191. }
  192. // Show error message
  193. function showError(message) {
  194. const status = document.getElementById('status');
  195. const result = document.getElementById('result');
  196. status.textContent = `Error: ${message}`;
  197. result.style.display = 'block';
  198. }
  199. // Validate URL format
  200. function validateUrl(url) {
  201. if (!url) return false;
  202. try {
  203. new URL(url);
  204. return true;
  205. } catch {
  206. return false;
  207. }
  208. }

10) style.css

  1. :root {
  2. --primary-color: #2196F3;
  3. --secondary-color: #1976D2;
  4. --background-color: #f5f5f5;
  5. --text-color: #333;
  6. --border-color: #ddd;
  7. }
  8. * {
  9. margin: 0;
  10. padding: 0;
  11. box-sizing: border-box;
  12. }
  13. body {
  14. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
  15. line-height: 1.6;
  16. color: var(--text-color);
  17. background-color: var(--background-color);
  18. }
  19. .container {
  20. max-width: 800px;
  21. margin: 0 auto;
  22. padding: 0 20px;
  23. }
  24. header {
  25. background-color: var(--primary-color);
  26. color: white;
  27. padding: 1rem 0;
  28. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  29. }
  30. header h1 {
  31. margin: 0;
  32. font-size: 1.5rem;
  33. }
  34. main {
  35. padding: 2rem 0;
  36. }
  37. .download-form {
  38. background: white;
  39. padding: 2rem;
  40. border-radius: 8px;
  41. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  42. }
  43. .form-group {
  44. margin-bottom: 1rem;
  45. }
  46. input[type="url"],
  47. select {
  48. width: 100%;
  49. padding: 0.75rem;
  50. border: 1px solid var(--border-color);
  51. border-radius: 4px;
  52. font-size: 1rem;
  53. }
  54. .audio-options {
  55. background: #f8f9fa;
  56. padding: 1rem;
  57. border-radius: 4px;
  58. margin-bottom: 1rem;
  59. }
  60. .download-btn {
  61. background-color: var(--primary-color);
  62. color: white;
  63. border: none;
  64. padding: 0.75rem 1.5rem;
  65. border-radius: 4px;
  66. cursor: pointer;
  67. font-size: 1rem;
  68. width: 100%;
  69. transition: background-color 0.2s;
  70. }
  71. .download-btn:hover {
  72. background-color: var(--secondary-color);
  73. }
  74. .download-btn:disabled {
  75. background-color: var(--border-color);
  76. cursor: not-allowed;
  77. }
  78. .result {
  79. margin-top: 1rem;
  80. padding: 1rem;
  81. border-radius: 4px;
  82. background: #f8f9fa;
  83. }
  84. .result-content {
  85. text-align: center;
  86. }
  87. #download-link {
  88. display: inline-block;
  89. margin-top: 0.5rem;
  90. color: var(--primary-color);
  91. text-decoration: none;
  92. font-weight: bold;
  93. }
  94. #download-link:hover {
  95. text-decoration: underline;
  96. }
  97. footer {
  98. text-align: center;
  99. padding: 2rem 0;
  100. color: #666;
  101. font-size: 0.9rem;
  102. }
  103. @media (max-width: 600px) {
  104. .download-form {
  105. padding: 1rem;
  106. }
  107. }
  108. /* 在 static/css/style.css 中添加 */
  109. .error-container {
  110. text-align: center;
  111. padding: 2rem;
  112. background: white;
  113. border-radius: 8px;
  114. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  115. margin: 2rem auto;
  116. max-width: 600px;
  117. }
  118. .error-container h1 {
  119. color: var(--primary-color);
  120. margin-bottom: 1rem;
  121. }
  122. .back-link {
  123. display: inline-block;
  124. margin-top: 1rem;
  125. padding: 0.5rem 1rem;
  126. background-color: var(--primary-color);
  127. color: white;
  128. text-decoration: none;
  129. border-radius: 4px;
  130. transition: background-color 0.2s;
  131. }
  132. .back-link:hover {
  133. background-color: var(--secondary-color);
  134. }
  135. /* 在 static/css/style.css 中添加 */
  136. .video-options,
  137. .audio-options,
  138. .advanced-options {
  139. background: #f8f9fa;
  140. padding: 1rem;
  141. border-radius: 4px;
  142. margin-bottom: 1rem;
  143. }
  144. .checkbox-label {
  145. display: flex;
  146. align-items: center;
  147. gap: 0.5rem;
  148. font-size: 0.9rem;
  149. color: #666;
  150. }
  151. .checkbox-label input[type="checkbox"] {
  152. width: auto;
  153. margin: 0;
  154. }

c. 部署 WEB UI

 到 docker-compose.yml 所在目录

运行: docker compose up -d

[/share/Multimedia/2024-MyProgramFiles/30.GitHub_Apps/3.cobalt-main/app] # docker compose up -d

成功后会看到:

使用:

  •  粘贴视频网站 URL
  • 选择: VA / Audio / Video no sound
  • 视频码率
  • 音频码率

在上传的 index.html 中取消  Advanced Options 部分,因为没测试,只是按照 api.me 的内容增加。 

如果只是下载视频,推荐用我另一下项目: Project-20 YT-DLP ,其功能近似。

连接:<Project-20 YT-DLP> 给视频网站下载工具 yt-dlp/yt-dlp 加个页面 python web v1.1 added subtitle , auto paste_python yt-dlp-CSDN博客

注:本文转载自blog.csdn.net的davenian的文章"https://blog.csdn.net/davenian/article/details/144994992"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

104
前端
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top