Aller au contenu

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 via PlatformModifiers.swift.

Flux de données

  1. Abonnement : FeedFetcher télécharge le flux → FeedParserParsedFeedStorageKit.LibraryStore.save(...) (upsert + déduplication par feedURL/guid).
  2. Lecture : AppModel.play résout l'URL (fichier local si téléchargé), la position de reprise et les réglages (volume, effets, identifiants de flux privé) → PlaybackController pilote SwitchingAudioEngine, qui route vers AVPlayer (streaming, radios) ou AVAudioEngine (fichiers locaux avec égaliseur / suppression des silences). NowPlayingCoordinator reflète l'état vers MPNowPlayingInfoCenter et reçoit les commandes de MPRemoteCommandCenter.
  3. 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.
  4. Sync : SyncKit échange abonnements et actions d'épisode via HTTP Basic ; AppModel.syncNow applique le distant puis pousse le local.

Couche UI (app)

  • RootViewNavigationSplitView (sidebar + détail), repli en pile sur iPhone
  • AppModel (@Observable, @MainActor) — état global et orchestration
  • PlaybackController, 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 (v1v10). Les secrets (mot de passe de synchronisation, identifiants de flux privés) ne sont jamais en base : ils vivent dans le Trousseau.