Architektur im Überblick
Quelldateien
Diese Seite wurde aus den folgenden Quelldateien erstellt:
- main.go
- utils/ampapi/token.go
- utils/runv2/runv2.go
- utils/runv3/runv3.go
- utils/task/track.go
- utils/ampapi/song.go
- utils/ampapi/album.go
- utils/ampapi/track.go
- utils/ampapi/search.go
- utils/ampapi/artist.go
- go.mod
- Dockerfile
- agent.js
- agent-arm64.js
- utils/task/album.go
- utils/task/station.go
- utils/lyrics/lyrics.go
- utils/task/playlist.go
- utils/ampapi/station.go
- utils/ampapi/playlist.go
Das Projekt apple-music-downloader ist eine in Go geschriebene Anwendung zum Herunterladen von Musikinhalten aus Apple Music. Die Architektur kombiniert serverseitige API-Interaktionen mit plattformspezifischen DRM-Entschlüsselungsmechanismen und unterstützt sowohl Desktop- als auch Android-Umgebungen.
Systemarchitektur
Die Gesamtarchitektur gliedert sich in fünf logische Schichten: API-Kommunikation, Aufgabenverwaltung, Download-Verarbeitung, DRM-Entschlüsselung und Konfiguration. Die API-Schicht (utils/ampapi) kapselt alle HTTP-Kommunikation mit den Apple Music Servern. Die Aufgabenverwaltung (utils/task) orchestriert Downloads für verschiedene Medientypen. Die Download-Verarbeitung (utils/runv2, utils/runv3) übernimmt HLS-Stream-Verarbeitung und Segment-Downloads. Die DRM-Schicht (agent.js, agent-arm64.js) ermöglicht FairPlay-Entschlüsselung auf Android-Geräten.
正在加载图表渲染器...
Architektur-Highlights:
- Token-basierte Authentifizierung: Alle API-Aufrufe benötigen einen Bearer-Token, der dynamisch von der Apple Music Website extrahiert wird (utils/ampapi/token.go:8-47)
- Modulare API-Struktur: Jeder Medientyp (Song, Album, Suche) besitzt ein eigenes Modul mit spezifischen Datenstrukturen und Endpunkten
- Zweigleisige Download-Architektur:
runv2für ungeschützte HLS-Streams,runv3für DRM-geschützte Inhalte mit Widevine-Unterstützung (utils/runv3/runv3.go:38-123) - Frida-basierte Android-Integration: JavaScript-Agenten interagieren direkt mit nativen Android-Bibliotheken für FairPlay-Entschlüsselung
API-Interaktion und Authentifizierung
Die API-Schicht bildet das Fundament der Anwendung und verwaltet die gesamte Kommunikation mit den Apple Music Servern. Das Modul utils/ampapi implementiert einen robusten Authentifizierungsmechanismus, der ohne feste API-Schlüssel auskommt.
Token-Extraktion
Die Funktion GetToken() extrahiert den für API-Aufrufe erforderlichen Bearer-Token dynamisch von der Apple Music Website:
go1func GetToken() (string, error) { 2 req, err := http.NewRequest("GET", "https://music.apple.com", nil) 3 // ... HTTP-Request ... 4 regex := regexp.MustCompile(`/assets/index~[^/]+\.js`) 5 indexJsUri := regex.FindString(string(body)) 6 // ... Lädt JavaScript-Datei ... 7 regex = regexp.MustCompile(`eyJh([^"]*)`) 8 token := regex.FindString(string(body)) 9 return token, nil 10}
Der Prozess durchläuft zwei Phasen: Zuerst wird die Hauptseite analysiert, um die URL der index~JavaScript-Datei zu extrahieren. Anschließend wird diese Datei geladen und nach einem JWT-Token (erkennbar am Präfix eyJh) durchsucht. Dieser Token wird für alle nachfolgenden API-Aufrufe im Authorization-Header verwendet.
API-Endpunkte und Datenstrukturen
Jeder Medientyp besitzt eine dedizierte Implementierung mit spezifischen Anfrageparametern und Antwortstrukturen. Das Song-Modul demonstriert das typische Muster:
go1func GetSongResp(storefront string, id string, language string, token string) (*SongResp, error) { 2 req, err := http.NewRequest("GET", 3 fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s", storefront, id), nil) 4 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 5 query := url.Values{} 6 query.Set("include", "albums,artists") 7 query.Set("extend", "extendedAssetUrls") 8 // ... 9}
Die API-Antworten werden in stark typisierte Go-Strukturen deserialisiert. Die SongRespData-Struktur enthält alle relevanten Metadaten einschließlich ExtendedAssetUrls, die für den Download-Prozess kritisch sind.
Suchfunktionalität
Das Suchmodul unterstützt die Abfrage verschiedener Medientypen mit Paginierung:
go1func Search(storefront, term, types, language, token string, limit, offset int) (*SearchResp, error) { 2 req, err := http.NewRequest("GET", 3 fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/search", storefront), nil) 4 query := url.Values{} 5 query.Set("term", term) 6 query.Set("types", types) // z.B. "songs,albums,artists" 7 query.Set("limit", fmt.Sprintf("%d", limit)) 8 // ... 9}
(utils/ampapi/search.go:11-92)
Die Suchergebnisse werden in einer hierarchischen Struktur zurückgegeben, die Songs, Alben und Künstler separat gruppiert.
Datenmodelle und Informationsmanagement
Die Datenmodelle bilden das Rückgrat der Anwendung und definieren, wie Metadaten und Download-Informationen durch das System fließen. Die Strukturen sind hierarchisch organisiert und spiegeln die Beziehungen zwischen verschiedenen Medientypen wider.
Track-Datenmodell
Die Track-Struktur in utils/task/track.go bündelt alle Informationen, die für einen einzelnen Download benötigt werden:
go1type Track struct { 2 ID string 3 Type string 4 Name string 5 Storefront string 6 Language string 7 SaveDir string 8 SaveName string 9 SavePath string 10 Codec string 11 M3u8 string 12 WebM3u8 string 13 Quality string 14 Resp ampapi.TrackRespData 15 PreType string // Übergeordneter Typ: Album oder Playlist 16 PreID string // Übergeordnete ID 17 AlbumData ampapi.AlbumRespData 18 PlaylistData ampapi.PlaylistRespData 19}
Dieses Design ermöglicht es, Tracks sowohl eigenständig als auch im Kontext ihres übergeordneten Albums oder einer Playlist zu verarbeiten. Die Methode GetAlbumData() lädt bei Bedarf zusätzliche Album-Informationen nach.
Album-Datenstruktur
Die AlbumRespData-Struktur repräsentiert die umfassendste Dateneinheit und enthält verschachtelte Beziehungen:
go1type AlbumRespData struct { 2 ID string `json:"id"` 3 Type string `json:"type"` 4 Attributes struct { 5 ArtistName string `json:"artistName"` 6 IsSingle bool `json:"isSingle"` 7 URL string `json:"url"` 8 TrackCount int `json:"trackCount"` 9 ReleaseDate string `json:"releaseDate"` 10 Name string `json:"name"` 11 RecordLabel string `json:"recordLabel"` 12 Upc string `json:"upc"` 13 AudioTraits []string `json:"audioTraits"` 14 // ... weitere Felder 15 } 16 Relationships struct { 17 Tracks struct { 18 Data []TrackRespData `json:"data"` 19 } `json:"tracks"` 20 Artists struct { 21 Data []ArtistRespData `json:"data"` 22 } `json:"artists"` 23 } 24}
(utils/ampapi/album.go:11-204)
Die Relationships-Struktur ermöglicht die Navigation zu verwandten Entitäten ohne zusätzliche API-Aufrufe, wenn die entsprechenden include-Parameter gesetzt wurden.
Track-API-Antwortstruktur
Die TrackRespData-Struktur deckt sowohl Songs als auch Music-Videos ab:
go1type TrackRespData struct { 2 ID string `json:"id"` 3 Type string `json:"type"` // "song" oder "music-video" 4 Attributes struct { 5 ExtendedAssetUrls struct { 6 EnhancedHls string `json:"enhancedHls"` 7 } `json:"extendedAssetUrls"` 8 HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` 9 DurationInMillis int `json:"durationInMillis"` 10 Name string `json:"name"` 11 AudioTraits []string `json:"audioTraits"` 12 // ... 13 } 14}
Das Feld ExtendedAssetUrls.EnhancedHls ist besonders wichtig, da es die URL für den HLS-Stream enthält, der für den Download verwendet wird.
Download- und Streaming-Architektur
Die Download-Architektur ist in zwei Varianten unterteilt: runv2 für einfache HLS-Downloads und runv3 für DRM-geschützte Inhalte mit Widevine-Integration. Beide Varianten folgen dem HLS-Protokoll (HTTP Live Streaming), unterscheiden sich jedoch in der Behandlung verschlüsselter Segmente.
HLS-Download-Implementierung (runv2)
Das Modul utils/runv2/runv2.go implementiert einen robusten HLS-Downloader mit Timeout-Management:
go1type TimedResponseBody struct { 2 timeout time.Duration 3 timer *time.Timer 4 threshold int 5 body io.Reader 6} 7 8func (b *TimedResponseBody) Read(p []byte) (int, error) { 9 n, err := b.body.Read(p) 10 if n >= b.threshold { 11 b.timer.Reset(b.timeout) 12 } 13 return n, err 14}
Die TimedResponseBody-Struktur implementiert einen adaptiven Timeout-Mechanismus: Der Timer wird zurückgesetzt, solange Daten mit einer Mindestgröße (threshold) empfangen werden. Dies verhindert Timeouts bei langsamen Verbindungen, solange der Download Fortschritte zeigt.
DRM-Entschlüsselung mit Widevine (runv3)
Das Modul utils/runv3/runv3.go erweitert die Download-Funktionalität um DRM-Unterstützung:
go1func getPSSH(contentId string, kidBase64 string) (string, error) { 2 kidBytes, err := base64.StdEncoding.DecodeString(kidBase64) 3 // ... Fehlerbehandlung ... 4 widevineCencHeader := &cdm.WidevineCencHeader{ 5 KeyId: [][]byte{kidBytes}, 6 Algorithm: &algo, 7 ContentId: []byte(contentIdEncoded), 8 } 9 widevineCenc, err := proto.Marshal(widevineCencHeader) 10 // ... 32-Byte-Präfix hinzufügen ... 11 return base64.StdEncoding.EncodeToString(widevineCenc), nil 12}
Die Funktion getPSSH() generiert die PSSH-Box (Protection System Specific Header), die für Widevine-Lizenzanforderungen benötigt wird. Der Prozess umfasst die Protobuf-Serialisierung des Widevine-Headers und das Hinzufügen eines 32-Byte-Präfixes.
Lizenzbeschaffung und Entschlüsselung
Der Lizenzierungsprozess erfolgt in zwei Phasen:
go1func BeforeRequest(cl *resty.Client, ctx context.Context, url string, body []byte) (*resty.Response, error) { 2 jsondata := map[string]interface{}{ 3 "challenge": base64.StdEncoding.EncodeToString(body), 4 "key-system": "com.widevine.alpha", 5 "uri": ctx.Value("uriPrefix").(string) + "," + ctx.Value("pssh").(string), 6 "adamId": ctx.Value("adamId").(string), 7 "isLibrary": false, 8 "user-initiated": true, 9 } 10 return cl.R().SetBody(jsondata).Post(url) 11} 12 13func AfterRequest(response *resty.Response) ([]byte, error) { 14 var responseData PlaybackLicense 15 json.Unmarshal(response.Body(), &responseData) 16 return base64.StdEncoding.DecodeString(responseData.License) 17}
BeforeRequest konstruiert die Lizenzanfrage mit der Challenge und PSSH-Box. AfterRequest extrahiert und dekodiert die Lizenz aus der Antwort.
Datenfluss für einen Track-Download
Der folgende Ablauf zeigt den vollständigen Download-Prozess für einen einzelnen Track von der URL-Eingabe bis zur fertigen Datei:
正在加载图表渲染器...
Datenfluss-Highlights:
- Token-Beschaffung (utils/ampapi/token.go:8-47): Erfolgt einmalig pro Sitzung durch Web-Scraping
- API-Metadaten (utils/ampapi/song.go:10-98): Liefern die M3U8-URL und Track-Informationen
- DRM-Verarbeitung (utils/runv3/runv3.go:38-123): Koordiniert Lizenzbeschaffung und Segment-Entschlüsselung
Utility-Funktionen und Programmkonfiguration
Die Datei main.go enthält zentrale Hilfsfunktionen für den Programmablauf, einschließlich URL-Validierung und String-Verarbeitung:
go1func checkUrl(url string) (string, string) { 2 pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music|classical\.music)\.apple\.com\/(\w{2})(?:\/album|\/album\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`) 3 matches := pat.FindAllStringSubmatch(url, -1) 4 if matches == nil { 5 return "", "" 6 } 7 return matches[0][1], matches[0][2] // storefront, id 8} 9 10func LimitString(s string) string { 11 if len([]rune(s)) > Config.LimitMax { 12 return string([]rune(s)[:Config.LimitMax]) 13 } 14 return s 15}
Die Funktion checkUrl() extrahiert Storefront (Ländercode) und ID aus Apple Music URLs. Sie unterstützt verschiedene URL-Formate einschließlich Beta- und Classical-Domains. LimitString() begrenzt String-Längen für Dateinamen, um Dateisystem-Beschränkungen zu respektieren.
Abhängigkeitsmanagement
Die go.mod-Datei definiert die technologische Basis der Anwendung:
| Abhängigkeit | Version | Zweck |
|---|---|---|
github.com/grafov/m3u8 | v0.11.1 | HLS-Playlist-Parsing |
github.com/go-resty/resty/v2 | v2.16.5 | HTTP-Client für API-Aufrufe |
google.golang.org/protobuf | v1.36.2 | Protobuf für Widevine-Header |
github.com/spf13/pflag | v1.0.5 | Kommandozeilen-Flag-Handling |
github.com/schollz/progressbar/v3 | v3.14.6 | Fortschrittsanzeige |
github.com/beevik/etree | v1.3.0 | XML-Verarbeitung |
gopkg.in/yaml.v2 | v2.2.8 | Konfigurationsdatei-Parsing |
github.com/fatih/color | v1.18.0 | Terminal-Farbausgabe |
Die Auswahl der Bibliotheken spiegelt die Anforderungen wider: HLS-Verarbeitung, HTTP-Kommunikation, DRM-Unterstützung und Benutzerfreundlichkeit.
Plattformübergreifende DRM-Unterstützung
Die DRM-Unterstützung für Android-Geräte wird durch Frida-Agenten realisiert, die direkt mit nativen Bibliotheken interagieren. Diese Agenten sind in JavaScript geschrieben und werden zur Laufzeit in den Apple Music Android-Prozess injiziert.
Frida-Agent-Architektur (x86)
Der Agent agent.js implementiert die FairPlay-Entschlüsselung durch Hooking nativer Funktionen:
javascript1const androidappmusic = Process.getModuleByName("libandroidappmusic.so"); 2const sessionCtrlPtr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl8instanceEv"); 3const getPersistentKeyAddr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl16getPersistentKeyE..."); 4const decryptContextAddr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl14decryptContextE..."); 5 6function getkdContext(adam, uri) { 7 const defaultId = newStdStringFromBuffer(adam); 8 const keyUri = newStdStringFromBuffer(uri); 9 const fpsCert = newStdString(fairplayCert); 10 getPersistentKey(persistentKey, sessionCtrlInstance, defaultId.str, keyUri.str, ...); 11 decryptContext(svfootHillPKey, sessionCtrlInstance, ptr); 12 return ap; 13}
Der Agent ruft Funktionen aus libandroidappmusic.so auf, um FairPlay-Schlüssel zu laden und Entschlüsselungskontexte zu erstellen. Die C++-Namen werden durch mangled Names identifiziert.
ARM64-Optimierung
Der ARM64-Agent verwendet dynamische Code-Generierung für Performance-Kritische Operationen:
javascript1function getPersistentKeyASM(sessionCtrlInstance, adamIdStr, keyUriStr, ...) { 2 const impl = malloc(Process.pageSize); 3 Memory.patchCode(impl, Process.pageSize, code => { 4 const writer = new Arm64Writer(code, { pc: impl }); 5 writer.putLdrRegAddress("x0", sessionCtrlInstance); 6 writer.putLdrRegAddress("x1", adamIdStr.str); 7 // ... Register setzen ... 8 writer.putCallAddressWithArguments(getPersistentKeyAddr, ["x0", "x1", ...]); 9 writer.flush(); 10 }); 11 const implFunc = new NativeFunction(impl, 'void', []); 12 implFunc(); 13}
Die ARM64-Variante generiert Maschinencode zur Laufzeit, um die komplexen Funktionsaufrufe mit vielen String-Parametern effizient abzubilden. Dies ist notwendig, da die Frida-Interception-API für Funktionen mit mehr als 8 Parametern eingeschränkt ist.
Java-Integration für M3U8-Beschaffung
Die Agenten bieten auch Java-Brücken für den Zugriff auf Android-APIs:
javascript1global.getM3U8 = function(adamID) { 2 Java.performNow(function () { 3 var SVPlaybackLeaseManagerProxy; 4 Java.choose("com.apple.android.music.playback.SVPlaybackLeaseManagerProxy", { 5 onMatch: function (x) { SVPlaybackLeaseManagerProxy = x }, 6 onComplete: function (x) {} 7 }); 8 var HLSParam = Java.array('java.lang.String', ["HLS"]) 9 var MediaAssetInfo = SVPlaybackLeaseManagerProxy.requestAsset(parseInt(adamID), HLSParam, false); 10 return MediaAssetInfo.getDownloadUrl(); 11 }); 12}
Diese Funktion nutzt die Java-Reflection-API, um M3U8-URLs direkt aus der Apple Music Android-App zu extrahieren, ohne die öffentliche API zu verwenden.
Modulabhängigkeiten
Die folgende Grafik zeigt die Abhängigkeitsbeziehungen zwischen den Kernmodulen:
正在加载图表渲染器...
Abhängigkeitsanalyse:
- main.go ist der zentrale Einstiegspunkt und koordiniert alle anderen Module
- utils/task fungiert als Orchestrator und verbindet API-Daten mit Download-Logik
- utils/runv3 hat die komplexesten Abhängigkeiten (API, DRM, Agent)
- Die DRM-Unterstützung ist optional und wird nur bei geschützten Inhalten aktiviert
Technologie-Stack und Design-Entscheidungen
| Technologie | Verwendungszweck | Begründung | Alternativen |
|---|---|---|---|
| Go 1.23 | Hauptsprache | Cross-Compilation, statische Binaries, gute HTTP-Bibliotheken | Rust, Python |
| Frida | Android-Instrumentierung | Dynamische Hooking-Möglichkeiten ohne Root | Xposed, Magisk-Module |
| Widevine CDM | DRM-Entschlüsselung | Industriestandard für Streaming-DRM | FairPlay (nur iOS/macOS) |
| HLS (m3u8) | Streaming-Protokoll | Von Apple Music verwendet | DASH, SmoothStreaming |
| Protobuf | Widevine-Header-Serialisierung | Binäres Format, effizient | JSON, XML |
| Resty | HTTP-Client | Einfache API, gute Testbarkeit | Standard net/http |
| m3u8-Bibliothek | Playlist-Parsing | Spezialisiert für HLS | Eigener Parser |
| pflag | CLI-Flags | POSIX-kompatibel, Go-Standard | cobra, urfave/cli |
Architekturentscheidungen
-
Separation of Concerns: Trennung von API-Kommunikation, Aufgabenverwaltung und Download-Logik ermöglicht unabhängige Tests und Wartung.
-
Dual-Track Download-Architektur:
runv2undrunv3als separate Module erlauben die Unterstützung verschiedener DRM-Szenarien ohne Komplexitätssteigerung. -
Frida-basierte Android-Unterstützung: Ermöglicht DRM-Entschlüsselung ohne Modifikation der Apple Music App, erfordert jedoch Frida-Server auf dem Gerät.
-
Dynamische Token-Extraktion: Vermeidet feste API-Schlüssel und funktioniert solange die Apple Music Website-Struktur ähnlich bleibt.
-
Strukturierte Fehlerbehandlung: Jedes Modul gibt explizite Fehler zurück, die vom Aufrufer behandelt werden können.
Bekannte Einschränkungen
- Token-Gültigkeit: Der extrahierte Bearer-Token hat eine begrenzte Laufzeit und muss regelmäßig erneuert werden
- Android-Anforderungen: Frida-Unterstützung erfordert ein gerootetes Gerät oder eine patchbare APK
- Rate-Limiting: Häufige API-Anfragen können zu temporären Sperren führen
- Regionalbeschränkungen: Verfügbarkeit hängt vom Storefront (Ländercode) ab
