From 80fa80ff09de539c13f050a0e986504af2c4b908 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 15 Nov 2025 17:37:17 +0100 Subject: [PATCH] feat(settings): filter pre-releases and add version compare Filter out pre-release GitHub releases (beta,, rc, preview, dev, canary, nightly) when resolving the latest app version and validate release names using semver. Sort stable releases by creation date and return the most recent stable release name. Add isVersionOutdated(current, latest) to compare coerced semver versions safely, handling invalid formats with a warning. Move version comparison logic from the settings component to the SettingsService and use the new helper to determine if the installed app is outdated. This prevents false positives from pre-release tags and improves version parsing robustness. --- apps/web/src/app/services/settings.service.ts | 77 +++++++++++++++++-- .../src/app/settings/settings.component.ts | 6 +- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/apps/web/src/app/services/settings.service.ts b/apps/web/src/app/services/settings.service.ts index deac6ddf5..a4ec4d7bf 100644 --- a/apps/web/src/app/services/settings.service.ts +++ b/apps/web/src/app/services/settings.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { StorageMap } from '@ngx-pwa/local-storage'; import { catchError, map, Observable } from 'rxjs'; +import * as semver from 'semver'; import { STORE_KEY, Theme } from 'shared-interfaces'; @Injectable({ @@ -52,6 +53,7 @@ export class SettingsService { /** * Returns the version of the released app + * Filters out pre-release versions (beta, alpha, rc) */ getAppVersion() { return this.http @@ -59,14 +61,48 @@ export class SettingsService { 'https://api.github.com/repos/4gray/iptvnator/releases' ) .pipe( - map( - (response) => - response.sort( - (a, b) => - new Date(b.created_at).getTime() - - new Date(a.created_at).getTime() - )[0] - ), + map((response) => { + // Filter out pre-release versions (beta, alpha, rc, etc.) + const stableReleases = response.filter((release) => { + const releaseName = release.name.toLowerCase(); + + // Check for beta/alpha/rc keywords in the release name + const prereleaseKeywords = [ + 'beta', + 'alpha', + 'rc', + 'preview', + 'dev', + 'canary', + 'nightly', + ]; + const hasPrereleaseKeyword = prereleaseKeywords.some( + (keyword) => releaseName.includes(keyword) + ); + + if (hasPrereleaseKeyword) { + return false; + } + + // Validate version format + const version = semver.valid( + semver.coerce(release.name) + ); + if (!version) return false; + + // Check if version has prerelease tags in semver format + return !semver.prerelease(release.name); + }); + + // Sort stable releases by creation date + const sortedReleases = stableReleases.sort( + (a, b) => + new Date(b.created_at).getTime() - + new Date(a.created_at).getTime() + ); + + return sortedReleases[0]; + }), map((response) => response.name), catchError((err) => { console.error(err); @@ -74,4 +110,29 @@ export class SettingsService { }) ); } + + /** + * Compares current version with latest version + * @param currentVersion current version of the app + * @param latestVersion latest stable version from GitHub + * @returns true if current version is outdated + */ + isVersionOutdated( + currentVersion: string, + latestVersion: string + ): boolean { + // Clean and coerce versions to handle invalid formats + const cleanCurrent = semver.coerce(currentVersion); + const cleanLatest = semver.coerce(latestVersion); + + if (!cleanCurrent || !cleanLatest) { + console.warn('Invalid version format:', { + currentVersion, + latestVersion, + }); + return false; + } + + return semver.lt(cleanCurrent, cleanLatest); + } } diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts index fa9fd33cf..5bc12672f 100644 --- a/apps/web/src/app/settings/settings.component.ts +++ b/apps/web/src/app/settings/settings.component.ts @@ -36,7 +36,6 @@ import { DialogService } from 'components'; import * as PlaylistActions from 'm3u-state'; import { selectIsEpgAvailable } from 'm3u-state'; import { take } from 'rxjs'; -import * as semver from 'semver'; import { DataService, EpgService, PlaylistsService } from 'services'; import { Language, @@ -242,7 +241,10 @@ export class SettingsComponent implements OnInit { */ isCurrentVersionOutdated(latestVersion: string): boolean { this.version = this.dataService.getAppVersion(); - return semver.lt(this.version, latestVersion); + return this.settingsService.isVersionOutdated( + this.version, + latestVersion + ); } /**