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.
This commit is contained in:
4gray committed 2025-11-15 17:37:17 +01:00
1 parent c34bdf73e1
commit 80fa80ff09
2 files changed
+73 -10

No files matched your search

+69 -8
View File
@@ -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);
}
}
@@ -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
);
}
/**