Skip to content

Commit

Permalink
Merge pull request #2 from fbngrmr/feat_support_collections_episode
Browse files Browse the repository at this point in the history
Feature: Support downloading collections and individual episodes
  • Loading branch information
fbngrmr authored Jan 13, 2023
2 parents ba7567f + 0853ceb commit 6283b2a
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 18 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<!-- ABOUT THE PROJECT -->
## 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.

<p align="right">(<a href="#top">back to top</a>)</p>

Expand Down Expand Up @@ -77,7 +77,10 @@
<!-- USAGE EXAMPLES -->
## 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/" \
Expand Down
176 changes: 159 additions & 17 deletions cmd/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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(
Expand All @@ -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)
}
}
}

0 comments on commit 6283b2a

Please sign in to comment.