feat(api): Add system controller and version check endpoint
This commit is contained in:
99
backend/src/controllers/systemController.ts
Normal file
99
backend/src/controllers/systemController.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import axios from "axios";
|
||||
import { Request, Response } from "express";
|
||||
import { logger } from "../utils/logger";
|
||||
import { VERSION } from "../version";
|
||||
|
||||
interface GithubRelease {
|
||||
tag_name: string;
|
||||
html_url: string;
|
||||
body: string;
|
||||
published_at: string;
|
||||
}
|
||||
|
||||
|
||||
// Helper to compare semantic versions (v1 > v2)
|
||||
const isNewerVersion = (latest: string, current: string): boolean => {
|
||||
try {
|
||||
const v1 = latest.split('.').map(Number);
|
||||
const v2 = current.split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
|
||||
const num1 = v1[i] || 0;
|
||||
const num2 = v2[i] || 0;
|
||||
if (num1 > num2) return true;
|
||||
if (num1 < num2) return false;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
// Fallback to string comparison if parsing fails
|
||||
return latest !== current;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLatestVersion = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const currentVersion = VERSION.number;
|
||||
const response = await axios.get<GithubRelease>(
|
||||
"https://api.github.com/repos/franklioxygen/mytube/releases/latest",
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "MyTube-App",
|
||||
},
|
||||
timeout: 5000, // 5 second timeout
|
||||
}
|
||||
);
|
||||
|
||||
const latestVersion = response.data.tag_name.replace(/^v/, "");
|
||||
const releaseUrl = response.data.html_url;
|
||||
|
||||
res.json({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
releaseUrl,
|
||||
hasUpdate: isNewerVersion(latestVersion, currentVersion),
|
||||
});
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
// Fallback: Try to get tags if no release is published
|
||||
try {
|
||||
const tagsResponse = await axios.get<any[]>(
|
||||
"https://api.github.com/repos/franklioxygen/mytube/tags",
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "MyTube-App",
|
||||
},
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
if (tagsResponse.data && tagsResponse.data.length > 0) {
|
||||
const latestTag = tagsResponse.data[0];
|
||||
const latestVersion = latestTag.name.replace(/^v/, "");
|
||||
const releaseUrl = `https://github.com/franklioxygen/mytube/releases/tag/${latestTag.name}`;
|
||||
const currentVersion = VERSION.number;
|
||||
|
||||
return res.json({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
releaseUrl,
|
||||
hasUpdate: isNewerVersion(latestVersion, currentVersion),
|
||||
});
|
||||
}
|
||||
} catch (tagError) {
|
||||
logger.warn("Failed to fetch tags as fallback:", tagError);
|
||||
}
|
||||
}
|
||||
|
||||
logger.error("Failed to check for updates:", error);
|
||||
// Return current version if check fails
|
||||
res.json({
|
||||
currentVersion: VERSION.number,
|
||||
latestVersion: VERSION.number,
|
||||
releaseUrl: "",
|
||||
hasUpdate: false,
|
||||
error: "Failed to check for updates",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import * as collectionController from "../controllers/collectionController";
|
||||
import * as downloadController from "../controllers/downloadController";
|
||||
import * as scanController from "../controllers/scanController";
|
||||
import * as subscriptionController from "../controllers/subscriptionController";
|
||||
import * as systemController from "../controllers/systemController";
|
||||
import * as videoController from "../controllers/videoController";
|
||||
import * as videoDownloadController from "../controllers/videoDownloadController";
|
||||
import * as videoMetadataController from "../controllers/videoMetadataController";
|
||||
@@ -162,4 +163,7 @@ router.delete(
|
||||
asyncHandler(cloudStorageController.clearThumbnailCacheEndpoint)
|
||||
);
|
||||
|
||||
// System routes
|
||||
router.get("/system/version", asyncHandler(systemController.getLatestVersion));
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
export const VERSION = {
|
||||
number: "1.1.0",
|
||||
number: "1.7.15",
|
||||
buildDate: new Date().toISOString().split("T")[0],
|
||||
name: "MyTube Backend Server",
|
||||
displayVersion: function () {
|
||||
|
||||
@@ -6,6 +6,7 @@ import App from '../App';
|
||||
// Mock axios
|
||||
vi.mock('axios', () => ({
|
||||
default: {
|
||||
create: vi.fn(() => ({
|
||||
get: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
post: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
put: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
@@ -13,7 +14,18 @@ vi.mock('axios', () => ({
|
||||
interceptors: {
|
||||
request: { use: vi.fn(), eject: vi.fn() },
|
||||
response: { use: vi.fn(), eject: vi.fn() }
|
||||
}
|
||||
},
|
||||
defaults: { headers: { common: {} } }
|
||||
})),
|
||||
get: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
post: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
put: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
delete: vi.fn(() => Promise.resolve({ data: {} })),
|
||||
interceptors: {
|
||||
request: { use: vi.fn(), eject: vi.fn() },
|
||||
response: { use: vi.fn(), eject: vi.fn() }
|
||||
},
|
||||
isAxiosError: vi.fn(() => false)
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import { GitHub } from '@mui/icons-material';
|
||||
import { Box, Container, Link, Typography, useTheme } from '@mui/material';
|
||||
import { Box, Chip, Container, Link, Tooltip, Typography, useTheme } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../utils/apiClient';
|
||||
|
||||
const Footer = () => {
|
||||
const theme = useTheme();
|
||||
const [updateInfo, setUpdateInfo] = useState<{
|
||||
hasUpdate: boolean;
|
||||
latestVersion: string;
|
||||
releaseUrl: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkVersion = async () => {
|
||||
try {
|
||||
const response = await api.get('/system/version');
|
||||
if (response.data && response.data.hasUpdate) {
|
||||
setUpdateInfo(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail for version check
|
||||
console.debug('Failed to check version:', error);
|
||||
}
|
||||
};
|
||||
|
||||
checkVersion();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -37,6 +59,28 @@ const Footer = () => {
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
v{import.meta.env.VITE_APP_VERSION}
|
||||
</Typography>
|
||||
{updateInfo?.hasUpdate && (
|
||||
<Tooltip title={`New version available: v${updateInfo.latestVersion}`}>
|
||||
<Link
|
||||
href={updateInfo.releaseUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{ ml: 1, textDecoration: 'none', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Chip
|
||||
label="Update"
|
||||
color="success"
|
||||
size="small"
|
||||
sx={{
|
||||
height: 16,
|
||||
fontSize: '0.65rem',
|
||||
cursor: 'pointer',
|
||||
'& .MuiChip-label': { px: 1 }
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||
Created by franklioxygen
|
||||
|
||||
Reference in New Issue
Block a user