feat: add country selection, cron automation, sparkle effects and layout fixes
This commit is contained in:
87
public/sw.js
87
public/sw.js
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user