Preise

Architektur im Überblick

Quelldateien

Diese Seite wurde aus den folgenden Quelldateien erstellt:

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: runv2 für ungeschützte HLS-Streams, runv3 fü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:

go
1func 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}

(utils/ampapi/token.go:8-47)

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:

go
1func 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}

(utils/ampapi/song.go:10-98)

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:

go
1func 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:

go
1type 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}

(utils/task/track.go:6-50)

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:

go
1type 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:

go
1type 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}

(utils/ampapi/track.go:2-49)

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:

go
1type 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}

(utils/runv2/runv2.go:26-65)

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:

go
1func 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}

(utils/runv3/runv3.go:38-123)

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:

go
1func 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}

(utils/runv3/runv3.go:38-123)

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:

Utility-Funktionen und Programmkonfiguration

Die Datei main.go enthält zentrale Hilfsfunktionen für den Programmablauf, einschließlich URL-Validierung und String-Verarbeitung:

go
1func 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}

(main.go:82-121)

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ängigkeitVersionZweck
github.com/grafov/m3u8v0.11.1HLS-Playlist-Parsing
github.com/go-resty/resty/v2v2.16.5HTTP-Client für API-Aufrufe
google.golang.org/protobufv1.36.2Protobuf für Widevine-Header
github.com/spf13/pflagv1.0.5Kommandozeilen-Flag-Handling
github.com/schollz/progressbar/v3v3.14.6Fortschrittsanzeige
github.com/beevik/etreev1.3.0XML-Verarbeitung
gopkg.in/yaml.v2v2.2.8Konfigurationsdatei-Parsing
github.com/fatih/colorv1.18.0Terminal-Farbausgabe

(go.mod:1-45)

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:

javascript
1const 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}

(agent.js:1-152)

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:

javascript
1function 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}

(agent-arm64.js:1-349)

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:

javascript
1global.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}

(agent.js:1-152)

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

TechnologieVerwendungszweckBegründungAlternativen
Go 1.23HauptspracheCross-Compilation, statische Binaries, gute HTTP-BibliothekenRust, Python
FridaAndroid-InstrumentierungDynamische Hooking-Möglichkeiten ohne RootXposed, Magisk-Module
Widevine CDMDRM-EntschlüsselungIndustriestandard für Streaming-DRMFairPlay (nur iOS/macOS)
HLS (m3u8)Streaming-ProtokollVon Apple Music verwendetDASH, SmoothStreaming
ProtobufWidevine-Header-SerialisierungBinäres Format, effizientJSON, XML
RestyHTTP-ClientEinfache API, gute TestbarkeitStandard net/http
m3u8-BibliothekPlaylist-ParsingSpezialisiert für HLSEigener Parser
pflagCLI-FlagsPOSIX-kompatibel, Go-Standardcobra, urfave/cli

Architekturentscheidungen

  1. Separation of Concerns: Trennung von API-Kommunikation, Aufgabenverwaltung und Download-Logik ermöglicht unabhängige Tests und Wartung.

  2. Dual-Track Download-Architektur: runv2 und runv3 als separate Module erlauben die Unterstützung verschiedener DRM-Szenarien ohne Komplexitätssteigerung.

  3. Frida-basierte Android-Unterstützung: Ermöglicht DRM-Entschlüsselung ohne Modifikation der Apple Music App, erfordert jedoch Frida-Server auf dem Gerät.

  4. Dynamische Token-Extraktion: Vermeidet feste API-Schlüssel und funktioniert solange die Apple Music Website-Struktur ähnlich bleibt.

  5. 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