Architecture¶
Skingomz sépare une logique métier pure (Swift Packages testables, sans UIKit/SwiftUI) d'une couche UI SwiftUI mince.
Skingomz (app SwiftUI, iOS/iPadOS/macOS)
│ dépend de
▼
Packages/Core
├─ PodcastModel types valeur : Podcast, Episode, PlayState
├─ FeedKit parsing RSS/Atom/iTunes/Podcasting 2.0 + FeedFetcher
├─ StorageKit SQLite via GRDB (abonnements, file, téléchargements, lecture)
├─ PlaybackKit moteurs audio (AVPlayer / AVAudioEngine) + PlaybackController
├─ DiscoveryKit recherche iTunes + fyyd + Radio Browser, parseur OPML
└─ SyncKit fournisseurs Nextcloud + gpodder.net
Principes¶
- Clean-room : le modèle et les parseurs sont écrits d'après les specs publiques
(RSS 2.0, Atom, namespaces
itunes:/podcast:, iTunes Search, protocole gpodder, OPML). Aucun code tiers copylefté n'est réutilisé. - Logique testable hors IDE : chaque package se teste avec
swift test(réseau abstrait derrière des protocoles → stubs dans les tests). - Cible native unique :
SDKROOT = auto, iOS + macOS depuis la même cible (pas de Mac Catalyst). Quelques modificateurs iOS-only sont neutralisés viaPlatformModifiers.swift.
Flux de données¶
- Abonnement :
FeedFetchertélécharge le flux →FeedParser→ParsedFeed→StorageKit.LibraryStore.save(...)(upsert + déduplication parfeedURL/guid). - Lecture :
AppModel.playrésout l'URL (fichier local si téléchargé), la position de reprise et les réglages (volume, effets, identifiants de flux privé) →PlaybackControllerpiloteSwitchingAudioEngine, qui route versAVPlayer(streaming, radios) ouAVAudioEngine(fichiers locaux avec égaliseur / suppression des silences).NowPlayingCoordinatorreflète l'état versMPNowPlayingInfoCenteret reçoit les commandes deMPRemoteCommandCenter. - Persistance de lecture : la position est sauvegardée périodiquement, à la
pause, au passage en arrière-plan et en fin de piste (
playstate), permettant la reprise et la synchronisation. Les radios (flux en direct) n'ont pas de position. - Sync :
SyncKitéchange abonnements et actions d'épisode via HTTP Basic ;AppModel.syncNowapplique le distant puis pousse le local.
Couche UI (app)¶
RootView—NavigationSplitView(sidebar + détail), repli en pile sur iPhoneAppModel(@Observable,@MainActor) — état global et orchestrationPlaybackController,DownloadController,SyncSettings— états dédiés, injectés via l'environnement SwiftUI
Persistance (SQLite / GRDB)¶
Tables principales : podcast (dont le marqueur isRadio), episode (+ index unique
podcastId,guid), et des tables séparées qui survivent au rafraîchissement des
flux : queue, download, playstate, podcast_pref. Migrations versionnées via
DatabaseMigrator (v1…v10). Les secrets (mot de passe de synchronisation,
identifiants de flux privés) ne sont jamais en base : ils vivent dans le Trousseau.