feat: add country selection, cron automation, sparkle effects and layout fixes

This commit is contained in:
2026-04-29 16:34:09 +02:00
parent c8f8c76e8a
commit 8bd9106ff3
11 changed files with 1130 additions and 52 deletions

View File

@@ -3,7 +3,7 @@
//
// This value is rewritten automatically before each build so deployed clients
// refresh to the newest shell and cached assets.
const CACHE_NAME = 'radioplayer-pwa-v5-1777463324180';
const CACHE_NAME = 'radioplayer-pwa-v5-1777473175316';
const STATION_SYNC_CACHE_NAME = 'radioplayer-station-sync-v1';
const MANAGED_CATALOG_CACHE_NAME = 'radioplayer-managed-catalog-v1';
const RADIO_BROWSER_API_ENDPOINT = 'https://de1.api.radio-browser.info/json/stations/search';
@@ -60,6 +60,8 @@ const RADIO_COUNTRIES = [
{ name: 'Turkey', code: 'TR' },
{ name: 'Ukraine', code: 'UA' },
];
const DEFAULT_SYNC_COUNTRY_CODES = RADIO_COUNTRIES.map((country) => country.code);
const MANAGED_COUNTRY_CODE = 'SI';
const MAX_TAGS = 12;
const OBVIOUSLY_UNSUPPORTED_CODECS = new Set([
'wma',
@@ -96,6 +98,7 @@ const IMAGE_FALLBACK_PATH = new URL('images/radio-placeholder.svg', self.registr
const SYNC_CATALOG_URL = new URL('data/radio-stations-sync.json', self.registration.scope).href;
const SYNC_CATALOG_PATH = new URL('data/radio-stations-sync.json', self.registration.scope).pathname;
const SYNC_META_URL = new URL('data/radio-stations-sync-meta.json', self.registration.scope).href;
const SYNC_SETTINGS_URL = new URL('data/radio-stations-sync-settings.json', self.registration.scope).href;
const SYNC_COUNTRY_PREFIX_PATH = new URL('data/countries/', self.registration.scope).pathname;
const BUNDLED_MANAGED_CATALOG_URL = new URL('stations.json', self.registration.scope).href;
const MANAGED_CATALOG_PATH = new URL('api/managed-stations.json', self.registration.scope).pathname;
@@ -190,6 +193,54 @@ async function writeJsonToCache(cache, requestUrl, payload) {
}));
}
function sanitizeSelectedCountryCodes(countryCodes) {
const uniqueCodes = Array.isArray(countryCodes)
? Array.from(new Set(
countryCodes
.map((entry) => (typeof entry === 'string' ? entry.trim().toUpperCase() : ''))
.filter((entry) => /^[A-Z]{2}$/.test(entry)),
))
: [];
if (!uniqueCodes.includes(MANAGED_COUNTRY_CODE)) {
uniqueCodes.unshift(MANAGED_COUNTRY_CODE);
}
return uniqueCodes.length > 0 ? uniqueCodes : [...DEFAULT_SYNC_COUNTRY_CODES];
}
async function readSyncSettings(cache) {
const cached = await cache.match(SYNC_SETTINGS_URL);
if (!cached) {
return { selectedCountryCodes: [...DEFAULT_SYNC_COUNTRY_CODES] };
}
try {
const payload = await cached.json();
return {
selectedCountryCodes: sanitizeSelectedCountryCodes(payload?.selectedCountryCodes),
};
} catch {
return { selectedCountryCodes: [...DEFAULT_SYNC_COUNTRY_CODES] };
}
}
async function writeSyncSettings(cache, countryCodes) {
const payload = {
selectedCountryCodes: sanitizeSelectedCountryCodes(countryCodes),
updatedAt: new Date().toISOString(),
};
await writeJsonToCache(cache, SYNC_SETTINGS_URL, payload);
return payload;
}
function getSyncCountries(selectedCountryCodes) {
return sanitizeSelectedCountryCodes(selectedCountryCodes).map((code) => {
const existing = RADIO_COUNTRIES.find((country) => country.code === code);
return existing || { name: code, code };
});
}
async function fetchCountryStations(country) {
const url = new URL(RADIO_BROWSER_API_ENDPOINT);
url.search = new URLSearchParams({
@@ -223,15 +274,19 @@ async function fetchCountryStations(country) {
.filter(Boolean);
}
async function syncRadioStations(reason = 'sync') {
async function syncRadioStations(reason = 'sync', selectedCountryCodesOverride = null) {
const syncCache = await caches.open(STATION_SYNC_CACHE_NAME);
const syncSettings = selectedCountryCodesOverride
? { selectedCountryCodes: sanitizeSelectedCountryCodes(selectedCountryCodesOverride) }
: await readSyncSettings(syncCache);
const syncCountries = getSyncCountries(syncSettings.selectedCountryCodes);
const aggregatedStations = [];
const seenStationIds = new Set();
const seenStreamUrls = new Set();
const failedCountries = [];
let syncedCountries = 0;
for (const country of RADIO_COUNTRIES) {
for (const country of syncCountries) {
try {
const stations = await fetchCountryStations(country);
await writeJsonToCache(syncCache, getCountryCatalogUrl(country.code), stations);
@@ -263,6 +318,7 @@ async function syncRadioStations(reason = 'sync') {
reason,
syncedAt: new Date().toISOString(),
countryCount: syncedCountries,
selectedCountryCodes: syncSettings.selectedCountryCodes,
failedCountries,
stationCount: aggregatedStations.length,
};
@@ -477,6 +533,31 @@ self.addEventListener('sync', (event) => {
event.waitUntil(syncRadioStations('background-sync'));
});
self.addEventListener('message', (event) => {
if (event.data?.type !== 'set-sync-countries') return;
event.waitUntil((async () => {
try {
const syncCache = await caches.open(STATION_SYNC_CACHE_NAME);
const syncSettings = await writeSyncSettings(syncCache, event.data?.countryCodes);
const syncResult = event.data?.syncNow
? await syncRadioStations('country-selection-update', syncSettings.selectedCountryCodes)
: null;
event.ports?.[0]?.postMessage({
ok: true,
selectedCountryCodes: syncSettings.selectedCountryCodes,
syncResult,
});
} catch (error) {
event.ports?.[0]?.postMessage({
ok: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
})());
});
self.addEventListener('periodicsync', (event) => {
if (event.tag === STATION_PERIODIC_SYNC_TAG) {
event.waitUntil(syncRadioStations('periodic-sync'));