diff --git a/README.md b/README.md index f615e17..9f76235 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ ## About The Project -`audiotheker` allows downloading all episodes of a program in the [ARD Audiothek](https://www.ardaudiothek.de/). It queries the official [GraphQL API](https://api.ardaudiothek.de/docs/#/GraphQL) to gather the download URLs. +`audiotheker` allows downloading all episodes of a program/collection or an individual episode in the [ARD Audiothek](https://www.ardaudiothek.de/). It queries the official [GraphQL API](https://api.ardaudiothek.de/docs/#/GraphQL) to gather the download URLs.

(back to top)

@@ -77,7 +77,10 @@ ## Usage +Copy the URL to a program, collection, or an individual episode from your browser and provide the URL and a target directory to the binary or a Docker container. + ### Built binary + ```sh $ ./audiotheker download \ "https://www.ardaudiothek.de/sendung/j-r-r-tolkien-der-herr-der-ringe-fantasy-hoerspiel-klassiker/12197351/" \ diff --git a/cmd/download.go b/cmd/download.go index 95b6eb7..a13e07f 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -16,6 +16,17 @@ import ( "github.com/spf13/cobra" ) +var GRAPHQL_ENDPOINT = "https://api.ardaudiothek.de/graphql" + +type QueryType int64 + +const ( + Episode QueryType = iota + Collection + Program + Unknown +) + func init() { rootCmd.AddCommand(downloadCmd) } @@ -58,7 +69,7 @@ func downloadFile(url string, targetDirectory string) (err error) { return nil } -func extractDownloadUrls(response *Response) []string { +func extractDownloadUrls(response *ItemsResponse) []string { var urls []string for _, nodes := range response.Result.Items.Nodes { @@ -72,13 +83,37 @@ func extractDownloadUrls(response *Response) []string { return urls } -func extractProgramId(url string) string { +func extractQueryId(url string) (string, QueryType) { normalizedUrl := strings.TrimSuffix(url, "/") - pattern := regexp.MustCompile(`^https:\/\/www\.ardaudiothek\.de\/sendung\/.*\/(\d*)$`) - return pattern.FindStringSubmatch(normalizedUrl)[1] + + // Program + programPattern := regexp.MustCompile(`^https:\/\/www\.ardaudiothek\.de\/sendung\/.*\/(\d*)$`) + matches := programPattern.FindStringSubmatch(normalizedUrl) + + if matches != nil { + return matches[1], Program + } + + // Collection + collectionPattern := regexp.MustCompile(`^https:\/\/www\.ardaudiothek\.de\/sammlung\/.*\/(\d*)$`) + matches = collectionPattern.FindStringSubmatch(normalizedUrl) + + if matches != nil { + return matches[1], Collection + } + + // Episode + episodePattern := regexp.MustCompile(`^https:\/\/www\.ardaudiothek\.de\/episode\/.*\/(\d*)$`) + matches = episodePattern.FindStringSubmatch(normalizedUrl) + + if matches != nil { + return matches[1], Episode + } + + return "", Unknown } -type Response struct { +type ItemsResponse struct { Result struct { Items struct { Nodes []struct { @@ -90,11 +125,22 @@ type Response struct { } } -func run(url string, targetDirectory string) { - programId := extractProgramId(url) +func sendGraphQlQuery(query string, variables map[string]interface{}, response interface{}) error { + client := graphql.NewClient(GRAPHQL_ENDPOINT, nil) - client := graphql.NewClient("https://api.ardaudiothek.de/graphql", nil) + rawGraphqlResponse, graphQlErr := client.ExecRaw(context.Background(), query, variables) + if graphQlErr != nil { + return graphQlErr + } + if jsonError := json.Unmarshal(rawGraphqlResponse, &response); jsonError != nil { + return jsonError + } + + return nil +} + +func getProgramUrls(queryId string) ([]string, error) { query := `query ProgramSetEpisodesQuery($id: ID!, $offset: Int!, $count: Int!) { result: programSet(id: $id) { items( @@ -111,30 +157,126 @@ func run(url string, targetDirectory string) { } } }` - variables := map[string]interface{}{ - "id": programId, + "id": queryId, "offset": 0, "count": 100, } - rawGraphqlResponse, graphQlErr := client.ExecRaw(context.Background(), query, variables) - if graphQlErr != nil { - panic(graphQlErr) + var response ItemsResponse + graphQlError := sendGraphQlQuery(query, variables, &response) + + if graphQlError != nil { + return nil, graphQlError } - var response Response - if jsonError := json.Unmarshal(rawGraphqlResponse, &response); jsonError != nil { - panic(jsonError) + urls := extractDownloadUrls(&response) + + return urls, nil +} + +func getCollectionUrls(queryId string) ([]string, error) { + query := `query EpisodesQuery($id: ID!, $offset: Int!, $limit: Int!) { + result: editorialCollection(id: $id, offset: $offset, limit: $limit) { + items { + nodes { + id + audios { + url + downloadUrl + allowDownload + } + } + } + } + }` + variables := map[string]interface{}{ + "id": queryId, + "offset": 0, + "limit": 100, + } + + var response ItemsResponse + graphQlError := sendGraphQlQuery(query, variables, &response) + + if graphQlError != nil { + return nil, graphQlError } urls := extractDownloadUrls(&response) + return urls, nil +} + +type ItemResponse struct { + Result struct { + Audios []struct { + DownloadUrl *string + } + } +} + +func getEpisodeUrls(queryId string) ([]string, error) { + query := `query EpisodeQuery($id: ID!) { + result: item(id: $id) { + audios { + downloadUrl + } + } + }` + variables := map[string]interface{}{ + "id": queryId, + } + + var response ItemResponse + graphQlError := sendGraphQlQuery(query, variables, &response) + + if graphQlError != nil { + return nil, graphQlError + } + + var urls []string + for _, audios := range response.Result.Audios { + if audios.DownloadUrl != nil { + if *audios.DownloadUrl != "" { + urls = append(urls, *audios.DownloadUrl) + } + } + } + + return urls, nil +} + +func getDownloadUrls(url string) ([]string, error) { + queryId, queryType := extractQueryId(url) + + switch queryType { + case Episode: + return getEpisodeUrls(queryId) + + case Collection: + return getCollectionUrls(queryId) + + case Program: + return getProgramUrls(queryId) + + default: + return nil, fmt.Errorf("URL is not supported: %s", url) + } +} + +func run(url string, targetDirectory string) { + urls, err := getDownloadUrls(url) + + if err != nil { + panic(err) + } + for _, url := range urls { downloadErr := downloadFile(url, targetDirectory) if downloadErr != nil { - fmt.Printf("Downloading URL %s failed with error: %v\n", url, downloadErr) + fmt.Printf("Downloading file %s failed with error: %v\n", url, downloadErr) } } }