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 + ); } /**