feat: Add Cloudflare Tunnel settings and service
This commit is contained in:
@@ -4,14 +4,29 @@ FROM node:22-alpine AS builder
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY package*.json ./
|
# Install dependencies
|
||||||
|
COPY backend/package*.json ./
|
||||||
# Skip Puppeteer download during build as we only need to compile TS
|
# Skip Puppeteer download during build as we only need to compile TS
|
||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
||||||
# Install build dependencies for native modules (python3, make, g++)
|
# Install build dependencies for native modules (python3, make, g++)
|
||||||
RUN apk add --no-cache python3 make g++
|
RUN apk add --no-cache python3 make g++
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
# Copy backend source
|
||||||
|
COPY backend/ .
|
||||||
|
|
||||||
|
# Copy frontend source for building
|
||||||
|
COPY frontend/ /app/frontend/
|
||||||
|
|
||||||
|
# Build frontend
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
# Install frontend dependencies
|
||||||
|
RUN npm ci
|
||||||
|
# Build frontend with relative paths
|
||||||
|
ENV VITE_API_URL=/api
|
||||||
|
ENV VITE_BACKEND_URL=
|
||||||
|
RUN npm run build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
# Build bgutil-ytdlp-pot-provider
|
# Build bgutil-ytdlp-pot-provider
|
||||||
WORKDIR /app/bgutil-ytdlp-pot-provider/server
|
WORKDIR /app/bgutil-ytdlp-pot-provider/server
|
||||||
@@ -42,6 +57,11 @@ RUN apk add --no-cache \
|
|||||||
RUN curl -fsSL https://deno.land/install.sh | sh -s -- -y && \
|
RUN curl -fsSL https://deno.land/install.sh | sh -s -- -y && \
|
||||||
ln -sf /root/.deno/bin/deno /usr/local/bin/deno
|
ln -sf /root/.deno/bin/deno /usr/local/bin/deno
|
||||||
|
|
||||||
|
# Install cloudflared (Binary download)
|
||||||
|
ARG TARGETARCH
|
||||||
|
RUN curl -L --output /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${TARGETARCH:-amd64} && \
|
||||||
|
chmod +x /usr/local/bin/cloudflared
|
||||||
|
|
||||||
# Install yt-dlp, bgutil-ytdlp-pot-provider, and yt-dlp-ejs for YouTube n challenge solving
|
# Install yt-dlp, bgutil-ytdlp-pot-provider, and yt-dlp-ejs for YouTube n challenge solving
|
||||||
RUN pip3 install yt-dlp bgutil-ytdlp-pot-provider yt-dlp-ejs --break-system-packages
|
RUN pip3 install yt-dlp bgutil-ytdlp-pot-provider yt-dlp-ejs --break-system-packages
|
||||||
|
|
||||||
@@ -52,11 +72,13 @@ ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
|
|||||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||||
|
|
||||||
# Install production dependencies only
|
# Install production dependencies only
|
||||||
COPY package*.json ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only=production
|
RUN npm ci --only=production
|
||||||
|
|
||||||
# Copy built artifacts from builder
|
# Copy built artifacts from builder
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
|
# Copy frontend build
|
||||||
|
COPY --from=builder /app/frontend/dist ./frontend/dist
|
||||||
# Copy drizzle migrations
|
# Copy drizzle migrations
|
||||||
COPY --from=builder /app/drizzle ./drizzle
|
COPY --from=builder /app/drizzle ./drizzle
|
||||||
# Copy bgutil-ytdlp-pot-provider
|
# Copy bgutil-ytdlp-pot-provider
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { Request, Response } from "express";
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {
|
import {
|
||||||
COLLECTIONS_DATA_PATH,
|
COLLECTIONS_DATA_PATH,
|
||||||
STATUS_DATA_PATH,
|
STATUS_DATA_PATH,
|
||||||
VIDEOS_DATA_PATH,
|
VIDEOS_DATA_PATH,
|
||||||
} from "../config/paths";
|
} from "../config/paths";
|
||||||
import { NotFoundError, ValidationError } from "../errors/DownloadErrors";
|
import { NotFoundError, ValidationError } from "../errors/DownloadErrors";
|
||||||
|
import { cloudflaredService } from "../services/cloudflaredService";
|
||||||
import downloadManager from "../services/downloadManager";
|
import downloadManager from "../services/downloadManager";
|
||||||
import * as loginAttemptService from "../services/loginAttemptService";
|
import * as loginAttemptService from "../services/loginAttemptService";
|
||||||
import * as storageService from "../services/storageService";
|
import * as storageService from "../services/storageService";
|
||||||
@@ -41,6 +42,8 @@ interface Settings {
|
|||||||
visitorMode?: boolean;
|
visitorMode?: boolean;
|
||||||
infiniteScroll?: boolean;
|
infiniteScroll?: boolean;
|
||||||
videoColumns?: number;
|
videoColumns?: number;
|
||||||
|
cloudflaredTunnelEnabled?: boolean;
|
||||||
|
cloudflaredToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSettings: Settings = {
|
const defaultSettings: Settings = {
|
||||||
@@ -310,6 +313,40 @@ export const updateSettings = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Cloudflare Tunnel settings changes
|
||||||
|
if (
|
||||||
|
newSettings.cloudflaredTunnelEnabled !==
|
||||||
|
existingSettings.cloudflaredTunnelEnabled ||
|
||||||
|
newSettings.cloudflaredToken !== existingSettings.cloudflaredToken
|
||||||
|
) {
|
||||||
|
// If we are enabling it (or it was enabled and config changed)
|
||||||
|
if (newSettings.cloudflaredTunnelEnabled) {
|
||||||
|
// Determine port
|
||||||
|
const port = process.env.PORT ? parseInt(process.env.PORT) : 5551;
|
||||||
|
|
||||||
|
const shouldRestart = existingSettings.cloudflaredTunnelEnabled;
|
||||||
|
|
||||||
|
if (shouldRestart) {
|
||||||
|
// If it was already enabled, we need to restart to apply changes (Token -> No Token, or vice versa)
|
||||||
|
if (newSettings.cloudflaredToken) {
|
||||||
|
cloudflaredService.restart(newSettings.cloudflaredToken);
|
||||||
|
} else {
|
||||||
|
cloudflaredService.restart(undefined, port);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It was disabled, now enabling -> just start
|
||||||
|
if (newSettings.cloudflaredToken) {
|
||||||
|
cloudflaredService.start(newSettings.cloudflaredToken);
|
||||||
|
} else {
|
||||||
|
cloudflaredService.start(undefined, port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If disabled, stop
|
||||||
|
cloudflaredService.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Apply settings immediately where possible
|
// Apply settings immediately where possible
|
||||||
downloadManager.setMaxConcurrentDownloads(newSettings.maxConcurrentDownloads);
|
downloadManager.setMaxConcurrentDownloads(newSettings.maxConcurrentDownloads);
|
||||||
|
|
||||||
@@ -873,3 +910,16 @@ export const restoreFromLastBackup = async (
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Cloudflare Tunnel status
|
||||||
|
* Errors are automatically handled by asyncHandler middleware
|
||||||
|
*/
|
||||||
|
export const getCloudflaredStatus = async (
|
||||||
|
_req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> => {
|
||||||
|
const status = cloudflaredService.getStatus();
|
||||||
|
res.json(status);
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,23 @@ import express from "express";
|
|||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import {
|
import {
|
||||||
checkCookies,
|
checkCookies,
|
||||||
cleanupBackupDatabases,
|
cleanupBackupDatabases,
|
||||||
deleteCookies,
|
deleteCookies,
|
||||||
deleteLegacyData,
|
deleteLegacyData,
|
||||||
exportDatabase,
|
exportDatabase,
|
||||||
formatFilenames,
|
formatFilenames,
|
||||||
getLastBackupInfo,
|
getCloudflaredStatus,
|
||||||
getPasswordEnabled,
|
getLastBackupInfo,
|
||||||
getSettings,
|
getPasswordEnabled,
|
||||||
importDatabase,
|
getSettings,
|
||||||
migrateData,
|
importDatabase,
|
||||||
restoreFromLastBackup,
|
migrateData,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
updateSettings,
|
restoreFromLastBackup,
|
||||||
uploadCookies,
|
updateSettings,
|
||||||
verifyPassword,
|
uploadCookies,
|
||||||
|
verifyPassword,
|
||||||
} from "../controllers/settingsController";
|
} from "../controllers/settingsController";
|
||||||
import { asyncHandler } from "../middleware/errorHandler";
|
import { asyncHandler } from "../middleware/errorHandler";
|
||||||
|
|
||||||
@@ -48,5 +49,6 @@ router.post(
|
|||||||
router.post("/cleanup-backup-databases", asyncHandler(cleanupBackupDatabases));
|
router.post("/cleanup-backup-databases", asyncHandler(cleanupBackupDatabases));
|
||||||
router.get("/last-backup-info", asyncHandler(getLastBackupInfo));
|
router.get("/last-backup-info", asyncHandler(getLastBackupInfo));
|
||||||
router.post("/restore-from-last-backup", asyncHandler(restoreFromLastBackup));
|
router.post("/restore-from-last-backup", asyncHandler(restoreFromLastBackup));
|
||||||
|
router.get("/cloudflared/status", asyncHandler(getCloudflaredStatus));
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -5,17 +5,19 @@ dotenv.config();
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import path from "path";
|
||||||
import {
|
import {
|
||||||
CLOUD_THUMBNAIL_CACHE_DIR,
|
CLOUD_THUMBNAIL_CACHE_DIR,
|
||||||
IMAGES_DIR,
|
IMAGES_DIR,
|
||||||
SUBTITLES_DIR,
|
SUBTITLES_DIR,
|
||||||
VIDEOS_DIR,
|
VIDEOS_DIR,
|
||||||
} from "./config/paths";
|
} from "./config/paths";
|
||||||
import { runMigrations } from "./db/migrate";
|
import { runMigrations } from "./db/migrate";
|
||||||
import { visitorModeMiddleware } from "./middleware/visitorModeMiddleware";
|
import { visitorModeMiddleware } from "./middleware/visitorModeMiddleware";
|
||||||
import { visitorModeSettingsMiddleware } from "./middleware/visitorModeSettingsMiddleware";
|
import { visitorModeSettingsMiddleware } from "./middleware/visitorModeSettingsMiddleware";
|
||||||
import apiRoutes from "./routes/api";
|
import apiRoutes from "./routes/api";
|
||||||
import settingsRoutes from "./routes/settingsRoutes";
|
import settingsRoutes from "./routes/settingsRoutes";
|
||||||
|
import { cloudflaredService } from "./services/cloudflaredService";
|
||||||
import downloadManager from "./services/downloadManager";
|
import downloadManager from "./services/downloadManager";
|
||||||
import * as storageService from "./services/storageService";
|
import * as storageService from "./services/storageService";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
@@ -80,6 +82,10 @@ const startServer = async () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Serve Frontend Static Files
|
||||||
|
const frontendDist = path.join(__dirname, "../../frontend/dist");
|
||||||
|
app.use(express.static(frontendDist));
|
||||||
|
|
||||||
// Cloud storage proxy endpoints
|
// Cloud storage proxy endpoints
|
||||||
// Proxy /cloud/videos/* and /cloud/images/* to Alist API
|
// Proxy /cloud/videos/* and /cloud/images/* to Alist API
|
||||||
@@ -206,6 +212,16 @@ const startServer = async () => {
|
|||||||
// Use separate middleware for settings that allows disabling visitor mode
|
// Use separate middleware for settings that allows disabling visitor mode
|
||||||
app.use("/api/settings", visitorModeSettingsMiddleware, settingsRoutes);
|
app.use("/api/settings", visitorModeSettingsMiddleware, settingsRoutes);
|
||||||
|
|
||||||
|
// SPA Fallback for Frontend
|
||||||
|
app.get("*", (req, res) => {
|
||||||
|
// Don't serve index.html for API calls that 404
|
||||||
|
if (req.path.startsWith('/api') || req.path.startsWith('/cloud')) {
|
||||||
|
res.status(404).send('Not Found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.sendFile(path.join(frontendDist, "index.html"));
|
||||||
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server running on port ${PORT}`);
|
console.log(`Server running on port ${PORT}`);
|
||||||
|
|
||||||
@@ -226,6 +242,18 @@ const startServer = async () => {
|
|||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
console.error("Failed to start metadata service:", err)
|
console.error("Failed to start metadata service:", err)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Start Cloudflared tunnel if enabled
|
||||||
|
const settings = storageService.getSettings();
|
||||||
|
if (settings.cloudflaredTunnelEnabled) {
|
||||||
|
if (settings.cloudflaredToken) {
|
||||||
|
cloudflaredService.start(settings.cloudflaredToken);
|
||||||
|
} else {
|
||||||
|
// Quick Tunnel
|
||||||
|
const port = typeof PORT === 'string' ? parseInt(PORT) : PORT;
|
||||||
|
cloudflaredService.start(undefined, port);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start server:", error);
|
console.error("Failed to start server:", error);
|
||||||
|
|||||||
121
backend/src/services/cloudflaredService.ts
Normal file
121
backend/src/services/cloudflaredService.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { ChildProcess, spawn } from 'child_process';
|
||||||
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
|
class CloudflaredService {
|
||||||
|
private process: ChildProcess | null = null;
|
||||||
|
private isRunning: boolean = false;
|
||||||
|
private tunnelId: string | null = null;
|
||||||
|
private accountTag: string | null = null;
|
||||||
|
private publicUrl: string | null = null;
|
||||||
|
|
||||||
|
private parseToken(token: string) {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.from(token, 'base64');
|
||||||
|
const decoded = JSON.parse(buffer.toString());
|
||||||
|
// Token format usually contains: a (account tag), t (tunnel id), s (secret)
|
||||||
|
this.accountTag = decoded.a || null;
|
||||||
|
this.tunnelId = decoded.t || null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to parse Cloudflare token', error);
|
||||||
|
this.tunnelId = null;
|
||||||
|
this.accountTag = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(token?: string, port: number = 5551) {
|
||||||
|
if (this.isRunning) {
|
||||||
|
logger.info('Cloudflared service is already running.');
|
||||||
|
if (token) this.parseToken(token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.publicUrl = null; // Reset URL
|
||||||
|
|
||||||
|
let args: string[] = [];
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
// Named Tunnel
|
||||||
|
this.parseToken(token);
|
||||||
|
logger.info(`Starting Cloudflared Named Tunnel (ID: ${this.tunnelId})...`);
|
||||||
|
args = ['tunnel', 'run', '--token', token];
|
||||||
|
} else {
|
||||||
|
// Quick Tunnel
|
||||||
|
this.tunnelId = null;
|
||||||
|
this.accountTag = null;
|
||||||
|
logger.info(`Starting Cloudflared Quick Tunnel on port ${port}...`);
|
||||||
|
args = ['tunnel', '--url', `http://localhost:${port}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.process = spawn('cloudflared', args);
|
||||||
|
|
||||||
|
const handleOutput = (data: Buffer) => {
|
||||||
|
const message = data.toString();
|
||||||
|
// Simple logging
|
||||||
|
logger.debug(`Cloudflared: ${message}`);
|
||||||
|
|
||||||
|
// Capture Quick Tunnel URL
|
||||||
|
// Example line: 2023-10-27T10:00:00Z INF | https://random-name.trycloudflare.com |
|
||||||
|
const urlMatch = message.match(/https?:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
|
||||||
|
if (urlMatch) {
|
||||||
|
this.publicUrl = urlMatch[0];
|
||||||
|
logger.info(`Cloudflared Quick Tunnel URL: ${this.publicUrl}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.process.stdout?.on('data', handleOutput);
|
||||||
|
this.process.stderr?.on('data', handleOutput); // Cloudflared often logs to stderr
|
||||||
|
|
||||||
|
this.process.on('close', (code) => {
|
||||||
|
logger.info(`Cloudflared exited with code ${code}`);
|
||||||
|
this.isRunning = false;
|
||||||
|
this.process = null;
|
||||||
|
this.publicUrl = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.process.on('error', (err) => {
|
||||||
|
logger.error('Failed to start Cloudflared process:', err);
|
||||||
|
this.isRunning = false;
|
||||||
|
this.process = null;
|
||||||
|
this.publicUrl = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
logger.info('Cloudflared process spawned.');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error spawning cloudflared:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
if (this.process) {
|
||||||
|
logger.info('Stopping Cloudflared tunnel...');
|
||||||
|
this.process.kill();
|
||||||
|
this.process = null;
|
||||||
|
this.isRunning = false;
|
||||||
|
this.publicUrl = null;
|
||||||
|
logger.info('Cloudflared tunnel stopped.');
|
||||||
|
} else {
|
||||||
|
logger.info('No Cloudflared process is running to stop.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public restart(token?: string, port: number = 5551) {
|
||||||
|
logger.info('Restarting Cloudflared tunnel...');
|
||||||
|
this.stop();
|
||||||
|
setTimeout(() => {
|
||||||
|
this.start(token, port);
|
||||||
|
}, 1000); // Wait a second before restarting
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStatus() {
|
||||||
|
return {
|
||||||
|
isRunning: this.isRunning,
|
||||||
|
tunnelId: this.tunnelId,
|
||||||
|
accountTag: this.accountTag,
|
||||||
|
publicUrl: this.publicUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cloudflaredService = new CloudflaredService();
|
||||||
176
build-and-push-test.sh
Executable file
176
build-and-push-test.sh
Executable file
@@ -0,0 +1,176 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DOCKER_PATH="/Applications/Docker.app/Contents/Resources/bin/docker"
|
||||||
|
USERNAME="franklioxygen"
|
||||||
|
|
||||||
|
# Default build arguments (can be overridden by environment variables)
|
||||||
|
VITE_API_URL=${VITE_API_URL:-"http://localhost:5551/api"}
|
||||||
|
VITE_BACKEND_URL=${VITE_BACKEND_URL:-"http://localhost:5551"}
|
||||||
|
|
||||||
|
# Define platforms to build
|
||||||
|
# Commented out arm64 as requested
|
||||||
|
PLATFORMS=("linux/amd64") # "linux/arm64")
|
||||||
|
|
||||||
|
# Tag definitions for TEST
|
||||||
|
BACKEND_TEST_AMD64="$USERNAME/mytube:backend-test-amd64"
|
||||||
|
# BACKEND_TEST_ARM64="$USERNAME/mytube:backend-test-arm64"
|
||||||
|
FRONTEND_TEST_AMD64="$USERNAME/mytube:frontend-test-amd64"
|
||||||
|
# FRONTEND_TEST_ARM64="$USERNAME/mytube:frontend-test-arm64"
|
||||||
|
|
||||||
|
# Ensure Docker is running
|
||||||
|
echo "🔍 Checking if Docker is running..."
|
||||||
|
$DOCKER_PATH ps > /dev/null 2>&1 || { echo "❌ Docker is not running. Please start Docker and try again."; exit 1; }
|
||||||
|
echo "✅ Docker is running!"
|
||||||
|
|
||||||
|
# Function to build backend for a specific platform
|
||||||
|
build_backend() {
|
||||||
|
local platform=$1
|
||||||
|
local tag=$2
|
||||||
|
|
||||||
|
echo "🏗️ Building backend for $platform..."
|
||||||
|
# Run build from root context to allow copying frontend files
|
||||||
|
# Use -f backend/Dockerfile to specify the Dockerfile path
|
||||||
|
$DOCKER_PATH build --no-cache --platform $platform -f backend/Dockerfile -t $tag .
|
||||||
|
|
||||||
|
echo "🚀 Pushing backend image: $tag"
|
||||||
|
$DOCKER_PATH push $tag
|
||||||
|
|
||||||
|
echo "🧹 Cleaning up local backend image: $tag"
|
||||||
|
$DOCKER_PATH rmi $tag
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to build frontend for a specific platform
|
||||||
|
build_frontend() {
|
||||||
|
local platform=$1
|
||||||
|
local tag=$2
|
||||||
|
|
||||||
|
echo "🏗️ Building frontend for $platform..."
|
||||||
|
cd frontend
|
||||||
|
$DOCKER_PATH build --no-cache --platform $platform \
|
||||||
|
--build-arg VITE_API_URL="$VITE_API_URL" \
|
||||||
|
--build-arg VITE_BACKEND_URL="$VITE_BACKEND_URL" \
|
||||||
|
-t $tag .
|
||||||
|
|
||||||
|
echo "🚀 Pushing frontend image: $tag"
|
||||||
|
$DOCKER_PATH push $tag
|
||||||
|
|
||||||
|
echo "🧹 Cleaning up local frontend image: $tag"
|
||||||
|
$DOCKER_PATH rmi $tag
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create and push manifest list
|
||||||
|
# create_and_push_manifest() {
|
||||||
|
# local manifest_tag=$1
|
||||||
|
# local image_amd64=$2
|
||||||
|
# local image_arm64=$3
|
||||||
|
#
|
||||||
|
# echo "📜 Creating manifest list: $manifest_tag"
|
||||||
|
# # Try to remove existing manifest first to avoid errors
|
||||||
|
# $DOCKER_PATH manifest rm $manifest_tag 2>/dev/null || true
|
||||||
|
#
|
||||||
|
# if ! $DOCKER_PATH manifest create $manifest_tag \
|
||||||
|
# --amend $image_amd64 \
|
||||||
|
# --amend $image_arm64 2>/dev/null; then
|
||||||
|
# echo "⚠️ Failed to create manifest list: $manifest_tag"
|
||||||
|
# echo " This might happen if the images are not yet available in the registry."
|
||||||
|
# echo " The platform-specific images are still available individually."
|
||||||
|
# return 1
|
||||||
|
# fi
|
||||||
|
#
|
||||||
|
# echo "🚀 Pushing manifest list: $manifest_tag"
|
||||||
|
# if ! $DOCKER_PATH manifest push $manifest_tag; then
|
||||||
|
# echo "⚠️ Failed to push manifest list: $manifest_tag"
|
||||||
|
# $DOCKER_PATH manifest rm $manifest_tag 2>/dev/null || true
|
||||||
|
# return 1
|
||||||
|
# fi
|
||||||
|
#
|
||||||
|
# echo "🧹 Cleaning up local manifest: $manifest_tag"
|
||||||
|
# $DOCKER_PATH manifest rm $manifest_tag 2>/dev/null || true
|
||||||
|
# return 0
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Build for each platform
|
||||||
|
echo "🏗️ Building TEST images for multiple platforms with separate tags..."
|
||||||
|
echo "Platforms: ${PLATFORMS[*]}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build backend for all platforms
|
||||||
|
for platform in "${PLATFORMS[@]}"; do
|
||||||
|
if [ "$platform" = "linux/amd64" ]; then
|
||||||
|
build_backend "$platform" "$BACKEND_TEST_AMD64"
|
||||||
|
elif [ "$platform" = "linux/arm64" ]; then
|
||||||
|
# build_backend "$platform" "$BACKEND_TEST_ARM64"
|
||||||
|
echo "Skipping arm64"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build frontend for all platforms
|
||||||
|
for platform in "${PLATFORMS[@]}"; do
|
||||||
|
if [ "$platform" = "linux/amd64" ]; then
|
||||||
|
build_frontend "$platform" "$FRONTEND_TEST_AMD64"
|
||||||
|
elif [ "$platform" = "linux/arm64" ]; then
|
||||||
|
# build_frontend "$platform" "$FRONTEND_TEST_ARM64"
|
||||||
|
echo "Skipping arm64"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create and push manifests
|
||||||
|
echo "📦 Tagging and pushing main test tags (amd64)..."
|
||||||
|
|
||||||
|
# MANIFEST_ERRORS=0
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
# if ! create_and_push_manifest "$USERNAME/mytube:backend-test" "$BACKEND_TEST_AMD64" "$BACKEND_TEST_ARM64"; then
|
||||||
|
# MANIFEST_ERRORS=$((MANIFEST_ERRORS + 1))
|
||||||
|
# fi
|
||||||
|
echo "🚀 Tagging and pushing backend test tag..."
|
||||||
|
$DOCKER_PATH pull "$BACKEND_TEST_AMD64" # Ensure we have it locally if it was cleaned up (though build cleaned it up, so we need to re-tag BEFORE cleanup or pull it again. Wait, build func cleans up.
|
||||||
|
# Ah, the build function cleans up: `$DOCKER_PATH rmi $tag`
|
||||||
|
# So $BACKEND_TEST_AMD64 is GONE locally.
|
||||||
|
# We need to re-pull it or NOT clean it up in the build function if we want to re-tag it.
|
||||||
|
# OR, we just tag it remotely? No, docker tag is local.
|
||||||
|
# We must pull it again or change the build function.
|
||||||
|
# Changing the build function is cleaner but risky if I don't want to change the structure too much.
|
||||||
|
# But calling pull is safe.
|
||||||
|
$DOCKER_PATH pull "$BACKEND_TEST_AMD64"
|
||||||
|
$DOCKER_PATH tag "$BACKEND_TEST_AMD64" "$USERNAME/mytube:backend-test"
|
||||||
|
$DOCKER_PATH push "$USERNAME/mytube:backend-test"
|
||||||
|
$DOCKER_PATH rmi "$USERNAME/mytube:backend-test"
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
# if ! create_and_push_manifest "$USERNAME/mytube:frontend-test" "$FRONTEND_TEST_AMD64" "$FRONTEND_TEST_ARM64"; then
|
||||||
|
# MANIFEST_ERRORS=$((MANIFEST_ERRORS + 1))
|
||||||
|
# fi
|
||||||
|
echo "🚀 Tagging and pushing frontend test tag..."
|
||||||
|
$DOCKER_PATH pull "$FRONTEND_TEST_AMD64"
|
||||||
|
$DOCKER_PATH tag "$FRONTEND_TEST_AMD64" "$USERNAME/mytube:frontend-test"
|
||||||
|
$DOCKER_PATH push "$USERNAME/mytube:frontend-test"
|
||||||
|
$DOCKER_PATH rmi "$USERNAME/mytube:frontend-test"
|
||||||
|
|
||||||
|
# if [ $MANIFEST_ERRORS -gt 0 ]; then
|
||||||
|
# echo ""
|
||||||
|
# echo "⚠️ Some manifest lists failed to create/push ($MANIFEST_ERRORS error(s))"
|
||||||
|
# echo " Platform-specific images are still available and can be used directly."
|
||||||
|
# fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Successfully built and pushed TEST images (amd64 only) to Docker Hub!"
|
||||||
|
echo ""
|
||||||
|
echo "Images:"
|
||||||
|
echo " - $USERNAME/mytube:backend-test (amd64)"
|
||||||
|
echo " - $USERNAME/mytube:frontend-test (amd64)"
|
||||||
|
echo ""
|
||||||
|
echo "Platform-specific images:"
|
||||||
|
echo " Backend:"
|
||||||
|
echo " - $BACKEND_TEST_AMD64"
|
||||||
|
# echo " - $BACKEND_TEST_ARM64"
|
||||||
|
echo " Frontend:"
|
||||||
|
echo " - $FRONTEND_TEST_AMD64"
|
||||||
|
# echo " - $FRONTEND_TEST_ARM64"
|
||||||
@@ -38,8 +38,9 @@ build_backend() {
|
|||||||
local version_tag=$3
|
local version_tag=$3
|
||||||
|
|
||||||
echo "🏗️ Building backend for $platform..."
|
echo "🏗️ Building backend for $platform..."
|
||||||
cd backend
|
# Run build from root context to allow copying frontend files
|
||||||
$DOCKER_PATH build --no-cache --platform $platform -t $tag .
|
# Use -f backend/Dockerfile to specify the Dockerfile path
|
||||||
|
$DOCKER_PATH build --no-cache --platform $platform -f backend/Dockerfile -t $tag .
|
||||||
|
|
||||||
if [ -n "$VERSION" ] && [ -n "$version_tag" ]; then
|
if [ -n "$VERSION" ] && [ -n "$version_tag" ]; then
|
||||||
$DOCKER_PATH tag $tag $version_tag
|
$DOCKER_PATH tag $tag $version_tag
|
||||||
@@ -58,8 +59,6 @@ build_backend() {
|
|||||||
if [ -n "$VERSION" ] && [ -n "$version_tag" ]; then
|
if [ -n "$VERSION" ] && [ -n "$version_tag" ]; then
|
||||||
$DOCKER_PATH rmi $version_tag
|
$DOCKER_PATH rmi $version_tag
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd ..
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to build frontend for a specific platform
|
# Function to build frontend for a specific platform
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
VITE_API_URL=http://localhost:5551/api
|
VITE_API_URL=/api
|
||||||
VITE_BACKEND_URL=http://localhost:5551
|
VITE_BACKEND_URL=
|
||||||
@@ -105,8 +105,8 @@ const CollectionThumbnail: React.FC<{ video: Video; index: number }> = ({ video,
|
|||||||
const isVideoInCloud = video.videoPath?.startsWith('cloud:') ?? false;
|
const isVideoInCloud = video.videoPath?.startsWith('cloud:') ?? false;
|
||||||
const thumbnailPathForCloud = isVideoInCloud ? video.thumbnailPath : null;
|
const thumbnailPathForCloud = isVideoInCloud ? video.thumbnailPath : null;
|
||||||
const thumbnailUrl = useCloudStorageUrl(thumbnailPathForCloud, 'thumbnail');
|
const thumbnailUrl = useCloudStorageUrl(thumbnailPathForCloud, 'thumbnail');
|
||||||
const localThumbnailUrl = !isVideoInCloud && video.thumbnailPath
|
const localThumbnailUrl = !isVideoInCloud && video.thumbnailPath
|
||||||
? `${import.meta.env.VITE_BACKEND_URL || 'http://localhost:5551'}${video.thumbnailPath}`
|
? `${import.meta.env.VITE_BACKEND_URL ?? 'http://localhost:5551'}${video.thumbnailPath}`
|
||||||
: undefined;
|
: undefined;
|
||||||
const src = thumbnailUrl || localThumbnailUrl || video.thumbnailUrl || 'https://via.placeholder.com/240x180?text=No+Thumbnail';
|
const src = thumbnailUrl || localThumbnailUrl || video.thumbnailUrl || 'https://via.placeholder.com/240x180?text=No+Thumbnail';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box, FormControl, FormControlLabel, InputLabel, MenuItem, Select, Switch, TextField, Typography } from '@mui/material';
|
import { Alert, Box, FormControl, FormControlLabel, InputLabel, MenuItem, Select, Switch, TextField, Typography } from '@mui/material';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
@@ -16,11 +16,13 @@ interface GeneralSettingsProps {
|
|||||||
savedVisitorMode?: boolean;
|
savedVisitorMode?: boolean;
|
||||||
infiniteScroll?: boolean;
|
infiniteScroll?: boolean;
|
||||||
videoColumns?: number;
|
videoColumns?: number;
|
||||||
|
cloudflaredTunnelEnabled?: boolean;
|
||||||
|
cloudflaredToken?: string;
|
||||||
onChange: (field: string, value: string | number | boolean) => void;
|
onChange: (field: string, value: string | number | boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GeneralSettings: React.FC<GeneralSettingsProps> = (props) => {
|
const GeneralSettings: React.FC<GeneralSettingsProps> = (props) => {
|
||||||
const { language, websiteName, showYoutubeSearch, visitorMode, savedVisitorMode, infiniteScroll, videoColumns, onChange } = props;
|
const { language, websiteName, showYoutubeSearch, visitorMode, savedVisitorMode, infiniteScroll, videoColumns, cloudflaredTunnelEnabled, cloudflaredToken, onChange } = props;
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -31,6 +33,18 @@ const GeneralSettings: React.FC<GeneralSettingsProps> = (props) => {
|
|||||||
const [remainingWaitTime, setRemainingWaitTime] = useState(0);
|
const [remainingWaitTime, setRemainingWaitTime] = useState(0);
|
||||||
const [baseError, setBaseError] = useState('');
|
const [baseError, setBaseError] = useState('');
|
||||||
|
|
||||||
|
// Poll for Cloudflare Tunnel status
|
||||||
|
const { data: cloudflaredStatus } = useQuery({
|
||||||
|
queryKey: ['cloudflaredStatus'],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!cloudflaredTunnelEnabled) return null;
|
||||||
|
const res = await axios.get(`${API_URL}/settings/cloudflared/status`);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
enabled: !!cloudflaredTunnelEnabled,
|
||||||
|
refetchInterval: 5000 // Poll every 5 seconds
|
||||||
|
});
|
||||||
|
|
||||||
// Use saved value for visibility, current value for toggle state
|
// Use saved value for visibility, current value for toggle state
|
||||||
const isVisitorMode = savedVisitorMode ?? visitorMode ?? false;
|
const isVisitorMode = savedVisitorMode ?? visitorMode ?? false;
|
||||||
|
|
||||||
@@ -231,6 +245,83 @@ const GeneralSettings: React.FC<GeneralSettingsProps> = (props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2 }}>
|
||||||
|
{t('cloudflaredTunnel')}
|
||||||
|
</Typography>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={cloudflaredTunnelEnabled ?? false}
|
||||||
|
onChange={(e) => onChange('cloudflaredTunnelEnabled', e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t('enableCloudflaredTunnel')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(cloudflaredTunnelEnabled) && (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={t('cloudflaredToken')}
|
||||||
|
type="password"
|
||||||
|
value={cloudflaredToken || ''}
|
||||||
|
onChange={(e) => onChange('cloudflaredToken', e.target.value)}
|
||||||
|
margin="normal"
|
||||||
|
helperText={t('cloudflaredTokenHelper') || "Paste your tunnel token here, or leave empty to use a random Quick Tunnel."}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cloudflaredTunnelEnabled && cloudflaredStatus && (
|
||||||
|
<Box sx={{ mt: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1, border: 1, borderColor: 'divider' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1, gap: 1 }}>
|
||||||
|
<Typography variant="subtitle2">Status:</Typography>
|
||||||
|
<Typography variant="body2" color={cloudflaredStatus.isRunning ? 'success.main' : 'error.main'} fontWeight="bold">
|
||||||
|
{cloudflaredStatus.isRunning ? 'Running' : 'Stopped'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{cloudflaredStatus.tunnelId && (
|
||||||
|
<Box sx={{ mb: 1 }}>
|
||||||
|
<Typography variant="subtitle2">Tunnel ID:</Typography>
|
||||||
|
<Typography variant="body2" fontFamily="monospace">
|
||||||
|
{cloudflaredStatus.tunnelId}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cloudflaredStatus.accountTag && (
|
||||||
|
<Box sx={{ mb: 1 }}>
|
||||||
|
<Typography variant="subtitle2">Account Tag:</Typography>
|
||||||
|
<Typography variant="body2" fontFamily="monospace">
|
||||||
|
{cloudflaredStatus.accountTag}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cloudflaredStatus.publicUrl && (
|
||||||
|
<Box sx={{ mb: 1 }}>
|
||||||
|
<Typography variant="subtitle2">Public URL:</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Typography variant="body2" fontFamily="monospace" sx={{ wordBreak: 'break-all' }}>
|
||||||
|
{cloudflaredStatus.publicUrl}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Alert severity="warning" sx={{ mt: 1, py: 0 }}>
|
||||||
|
Quick Tunnel URLs change every time the tunnel restarts.
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!cloudflaredStatus.publicUrl && (
|
||||||
|
<Alert severity="info" sx={{ mt: 1 }}>
|
||||||
|
Public hostname is managed in your Cloudflare Zero Trust Dashboard.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
@@ -256,7 +347,7 @@ const GeneralSettings: React.FC<GeneralSettingsProps> = (props) => {
|
|||||||
error={passwordError}
|
error={passwordError}
|
||||||
isLoading={isVerifyingPassword}
|
isLoading={isVerifyingPassword}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const VideoCard: React.FC<VideoCardProps> = ({
|
|||||||
const thumbnailPathForCloud = isVideoInCloud ? video.thumbnailPath : null;
|
const thumbnailPathForCloud = isVideoInCloud ? video.thumbnailPath : null;
|
||||||
const thumbnailUrl = useCloudStorageUrl(thumbnailPathForCloud, 'thumbnail');
|
const thumbnailUrl = useCloudStorageUrl(thumbnailPathForCloud, 'thumbnail');
|
||||||
const localThumbnailUrl = !isVideoInCloud && video.thumbnailPath
|
const localThumbnailUrl = !isVideoInCloud && video.thumbnailPath
|
||||||
? `${import.meta.env.VITE_BACKEND_URL || 'http://localhost:5551'}${video.thumbnailPath}`
|
? `${import.meta.env.VITE_BACKEND_URL ?? 'http://localhost:5551'}${video.thumbnailPath}`
|
||||||
: undefined;
|
: undefined;
|
||||||
const thumbnailSrc = thumbnailUrl || localThumbnailUrl || video.thumbnailUrl;
|
const thumbnailSrc = thumbnailUrl || localThumbnailUrl || video.thumbnailUrl;
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ const SidebarThumbnail: React.FC<{ video: Video }> = ({ video }) => {
|
|||||||
const isVideoInCloud = video.videoPath?.startsWith('cloud:') ?? false;
|
const isVideoInCloud = video.videoPath?.startsWith('cloud:') ?? false;
|
||||||
const thumbnailPathForCloud = isVideoInCloud ? video.thumbnailPath : null;
|
const thumbnailPathForCloud = isVideoInCloud ? video.thumbnailPath : null;
|
||||||
const thumbnailUrl = useCloudStorageUrl(thumbnailPathForCloud, 'thumbnail');
|
const thumbnailUrl = useCloudStorageUrl(thumbnailPathForCloud, 'thumbnail');
|
||||||
const localThumbnailUrl = !isVideoInCloud && video.thumbnailPath
|
const localThumbnailUrl = !isVideoInCloud && video.thumbnailPath
|
||||||
? `${import.meta.env.VITE_BACKEND_URL || 'http://localhost:5551'}${video.thumbnailPath}`
|
? `${import.meta.env.VITE_BACKEND_URL ?? 'http://localhost:5551'}${video.thumbnailPath}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const useCloudStorageUrl = (
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Regular path, construct URL synchronously
|
// Regular path, construct URL synchronously
|
||||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5551';
|
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL ?? 'http://localhost:5551';
|
||||||
setUrl(`${BACKEND_URL}${path}`);
|
setUrl(`${BACKEND_URL}${path}`);
|
||||||
}
|
}
|
||||||
}, [path, type]);
|
}, [path, type]);
|
||||||
|
|||||||
@@ -481,6 +481,8 @@ const SettingsPage: React.FC = () => {
|
|||||||
savedVisitorMode={settingsData?.visitorMode}
|
savedVisitorMode={settingsData?.visitorMode}
|
||||||
infiniteScroll={settings.infiniteScroll}
|
infiniteScroll={settings.infiniteScroll}
|
||||||
videoColumns={settings.videoColumns}
|
videoColumns={settings.videoColumns}
|
||||||
|
cloudflaredTunnelEnabled={settings.cloudflaredTunnelEnabled}
|
||||||
|
cloudflaredToken={settings.cloudflaredToken}
|
||||||
onChange={(field, value) => handleChange(field as keyof Settings, value)}
|
onChange={(field, value) => handleChange(field as keyof Settings, value)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -82,4 +82,6 @@ export interface Settings {
|
|||||||
visitorMode?: boolean;
|
visitorMode?: boolean;
|
||||||
infiniteScroll?: boolean;
|
infiniteScroll?: boolean;
|
||||||
videoColumns?: number;
|
videoColumns?: number;
|
||||||
|
cloudflaredTunnelEnabled?: boolean;
|
||||||
|
cloudflaredToken?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5551';
|
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL ?? 'http://localhost:5551';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a path is a cloud storage path (starts with "cloud:")
|
* Check if a path is a cloud storage path (starts with "cloud:")
|
||||||
|
|||||||
@@ -561,6 +561,12 @@ export const en = {
|
|||||||
moveThumbnailsToVideoFolderDescription:
|
moveThumbnailsToVideoFolderDescription:
|
||||||
"When enabled, thumbnail files will be moved to the same folder as the video file. When disabled, they will be moved to the isolated images folder.",
|
"When enabled, thumbnail files will be moved to the same folder as the video file. When disabled, they will be moved to the isolated images folder.",
|
||||||
|
|
||||||
|
// Cloudflare Tunnel
|
||||||
|
cloudflaredTunnel: "Cloudflare Tunnel",
|
||||||
|
enableCloudflaredTunnel: "Enable Cloudflare Tunnel",
|
||||||
|
cloudflaredToken: "Tunnel Token",
|
||||||
|
cloudflaredTokenHelper: "Paste your tunnel token here, or leave empty to use a random Quick Tunnel.",
|
||||||
|
|
||||||
// Database Export/Import
|
// Database Export/Import
|
||||||
exportImportDatabase: "Export/Import Database",
|
exportImportDatabase: "Export/Import Database",
|
||||||
exportImportDatabaseDescription:
|
exportImportDatabaseDescription:
|
||||||
|
|||||||
@@ -543,6 +543,12 @@ export const zh = {
|
|||||||
moveThumbnailsToVideoFolderDescription:
|
moveThumbnailsToVideoFolderDescription:
|
||||||
"启用后,封面文件将被移动到与视频文件相同的文件夹中。禁用后,它们将被移动到独立的图片文件夹中。",
|
"启用后,封面文件将被移动到与视频文件相同的文件夹中。禁用后,它们将被移动到独立的图片文件夹中。",
|
||||||
|
|
||||||
|
// Cloudflare Tunnel
|
||||||
|
cloudflaredTunnel: "Cloudflare 穿透",
|
||||||
|
enableCloudflaredTunnel: "启用 Cloudflare 穿透",
|
||||||
|
cloudflaredToken: "Token",
|
||||||
|
cloudflaredTokenHelper: "在此粘贴您的 Token,或留空以使用随机 Quick Tunnel。",
|
||||||
|
|
||||||
// Database Export/Import
|
// Database Export/Import
|
||||||
exportImportDatabase: "导出/导入数据库",
|
exportImportDatabase: "导出/导入数据库",
|
||||||
exportImportDatabaseDescription:
|
exportImportDatabaseDescription:
|
||||||
|
|||||||
@@ -12,6 +12,33 @@ export default defineConfig({
|
|||||||
interval: 2000,
|
interval: 2000,
|
||||||
ignored: ['/node_modules/']
|
ignored: ['/node_modules/']
|
||||||
},
|
},
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:5551',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
'/cloud': {
|
||||||
|
target: 'http://localhost:5551',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
'/images': {
|
||||||
|
target: 'http://localhost:5551',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
'/videos': {
|
||||||
|
target: 'http://localhost:5551',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
'/subtitles': {
|
||||||
|
target: 'http://localhost:5551',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version)
|
'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version)
|
||||||
|
|||||||
Reference in New Issue
Block a user