From 805c4fabc955be31e976fa656d4bc49f3b9c19b8 Mon Sep 17 00:00:00 2001 From: Tom Wheeler Date: Tue, 26 Aug 2025 01:04:32 +1200 Subject: [PATCH] feat: add version checking --- server/api/github.ts | 133 ++++++++++++++++++++++++++++++++++++ server/index.ts | 4 ++ server/routes/index.ts | 49 +++++++++++-- server/utils/restartFlag.ts | 23 +++++++ 4 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 server/api/github.ts create mode 100644 server/utils/restartFlag.ts diff --git a/server/api/github.ts b/server/api/github.ts new file mode 100644 index 0000000..72089d2 --- /dev/null +++ b/server/api/github.ts @@ -0,0 +1,133 @@ +import cacheManager from '@server/lib/cache'; +import logger from '@server/logger'; +import ExternalAPI from './externalapi'; + +interface GitHubRelease { + url: string; + assets_url: string; + upload_url: string; + html_url: string; + id: number; + node_id: string; + tag_name: string; + target_commitish: string; + name: string; + draft: boolean; + prerelease: boolean; + created_at: string; + published_at: string; + tarball_url: string; + zipball_url: string; + body: string; +} + +interface GithubCommit { + sha: string; + node_id: string; + commit: { + author: { + name: string; + email: string; + date: string; + }; + committer: { + name: string; + email: string; + date: string; + }; + message: string; + tree: { + sha: string; + url: string; + }; + url: string; + comment_count: number; + verification: { + verified: boolean; + reason: string; + signature: string; + payload: string; + }; + }; + url: string; + html_url: string; + comments_url: string; + parents: [ + { + sha: string; + url: string; + html_url: string; + } + ]; +} + +class GithubAPI extends ExternalAPI { + constructor() { + super( + 'https://api.github.com', + {}, + { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + nodeCache: cacheManager.getCache('github').data, + } + ); + } + + public async getAgregarrReleases({ + take = 20, + }: { + take?: number; + } = {}): Promise { + try { + const data = await this.get( + '/repos/agregarr/agregarr/releases', + { + params: { + per_page: take, + }, + } + ); + + return data; + } catch (e) { + logger.warn( + "Failed to retrieve GitHub releases. This may be an issue on GitHub's end. Agregarr can't check if it's on the latest version.", + { label: 'GitHub API', errorMessage: e.message } + ); + return []; + } + } + + public async getAgregarrCommits({ + take = 20, + branch = 'develop', + }: { + take?: number; + branch?: string; + } = {}): Promise { + try { + const data = await this.get( + '/repos/agregarr/agregarr/commits', + { + params: { + per_page: take, + branch, + }, + } + ); + + return data; + } catch (e) { + logger.warn( + "Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Agregarr can't check if it's on the latest version.", + { label: 'GitHub API', errorMessage: e.message } + ); + return []; + } + } +} + +export default GithubAPI; diff --git a/server/index.ts b/server/index.ts index aecbd4f..3362719 100644 --- a/server/index.ts +++ b/server/index.ts @@ -6,6 +6,7 @@ import { startJobs } from '@server/job/schedule'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import routes from '@server/routes'; +import restartFlag from '@server/utils/restartFlag'; // imageproxy removed - not needed for collections-only app import { getAppVersion } from '@server/utils/appVersion'; import { getClientIp } from '@supercharge/request-ip'; @@ -44,6 +45,9 @@ app // Load Settings const settings = getSettings().load(); + // Initialize RestartFlag with current settings + restartFlag.initializeSettings(settings.main); + // Initialize sync status for existing collections (one-time migration) settings.initializeSyncStatusForExistingCollections(); diff --git a/server/routes/index.ts b/server/routes/index.ts index e52851e..4c621cd 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,4 +1,5 @@ // PushoverAPI removed - notification system not needed +import GithubAPI from '@server/api/github'; import TheMovieDb from '@server/api/themoviedb'; import type { TmdbMovieResult, @@ -12,6 +13,7 @@ import { mapNetwork } from '@server/models/Tv'; import settingsRoutes from '@server/routes/settings'; import { appDataPath, appDataStatus } from '@server/utils/appDataVolume'; import { getAppVersion, getCommitTag } from '@server/utils/appVersion'; +import restartFlag from '@server/utils/restartFlag'; import { isPerson } from '@server/utils/typeHelpers'; import { Router } from 'express'; import authRoutes from './auth'; @@ -39,14 +41,51 @@ const router = Router(); router.use(checkUser); -router.get('/status', (_req, res) => { +router.get('/status', async (_req, res) => { + const githubApi = new GithubAPI(); + + const currentVersion = getAppVersion(); + const commitTag = getCommitTag(); + let updateAvailable = false; + let commitsBehind = 0; + + if (currentVersion.startsWith('develop-') && commitTag !== 'local') { + const commits = await githubApi.getAgregarrCommits(); + + if (commits.length) { + const filteredCommits = commits.filter( + (commit) => !commit.commit.message.includes('[skip ci]') + ); + if (filteredCommits[0].sha !== commitTag) { + updateAvailable = true; + } + + const commitIndex = filteredCommits.findIndex( + (commit) => commit.sha === commitTag + ); + + if (updateAvailable) { + commitsBehind = commitIndex; + } + } + } else if (commitTag !== 'local') { + const releases = await githubApi.getAgregarrReleases(); + + if (releases.length) { + const latestVersion = releases[0]; + + if (!latestVersion.name.includes(currentVersion)) { + updateAvailable = true; + } + } + } + return res.status(200).json({ version: getAppVersion(), - status: 'ok', commitTag: getCommitTag(), - tz: Intl.DateTimeFormat().resolvedOptions().timeZone, - appData: appDataStatus(), - appDataPath: appDataPath(), + updateAvailable, + commitsBehind, + restartRequired: restartFlag.isSet(), }); }); diff --git a/server/utils/restartFlag.ts b/server/utils/restartFlag.ts new file mode 100644 index 0000000..387ec5c --- /dev/null +++ b/server/utils/restartFlag.ts @@ -0,0 +1,23 @@ +import type { MainSettings } from '@server/lib/settings'; +import { getSettings } from '@server/lib/settings'; + +class RestartFlag { + private settings: MainSettings; + + public initializeSettings(settings: MainSettings): void { + this.settings = { ...settings }; + } + + public isSet(): boolean { + const settings = getSettings().main; + + return ( + this.settings.csrfProtection !== settings.csrfProtection || + this.settings.trustProxy !== settings.trustProxy + ); + } +} + +const restartFlag = new RestartFlag(); + +export default restartFlag;