Skip to content

Instantly share code, notes, and snippets.

@mtfurlan
Created December 8, 2024 02:39
Show Gist options
  • Select an option

  • Save mtfurlan/1fa16b4d00aff25dd32c5cbb2fe795fb to your computer and use it in GitHub Desktop.

Select an option

Save mtfurlan/1fa16b4d00aff25dd32c5cbb2fe795fb to your computer and use it in GitHub Desktop.

Revisions

  1. mtfurlan created this gist Dec 8, 2024.
    184 changes: 184 additions & 0 deletions mixcloud.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,184 @@
    #!/usr/bin/env bash
    set -euo pipefail

    # shellcheck disable=SC2120
    h () {
    # if arguments, print them
    [ $# == 0 ] || echo "$*"

    cat <<EOF
    Usage: $(basename "${BASH_SOURCE[0]}") [OPTION]... <album> <mixcloud url>
    download a mix from mixcloud and try to tag properly
    album
    Available options:
    -h, --help display this help and exit
    EOF

    # if args, exit 1 else exit 0
    [ $# == 0 ] || exit 1
    exit 0
    }
    msg() {
    echo >&2 -e "${*-}"
    }

    die() {
    local msg=$1
    local code=${2-1} # default exit status 1
    msg "$msg"
    exit "$code"
    }

    # getopt short options go together, long options have commas
    TEMP=$(getopt -o h --long help -n "$0" -- "$@")
    #shellcheck disable=SC2181
    if [ $? != 0 ] ; then
    die "something wrong with getopt"
    fi
    eval set -- "$TEMP"

    while true ; do
    case "$1" in
    -h|--help) h ;;
    --) shift ; break ;;
    *) die "issue parsing args, unexpected argument '$1'!" ;;
    esac
    done
    album=${1:-}
    if [ -z "$album" ]; then
    h "need to pass in album"
    fi
    url=${2:-}
    if [ -z "$url" ]; then
    h "need to pass in target url"
    fi

    #url looks like https://www.mixcloud.com/Ishkur/the-ultimate-hi-nrg-mix/
    if [[ "$url" =~ https:\/\/www.mixcloud.com\/([^\/]*)\/([^\/]*)\/?$ ]]; then
    username="${BASH_REMATCH[1]}"
    slug="${BASH_REMATCH[2]}"
    else
    help "can't match url, either use better input or fix mathcing logic in script"
    fi

    msg "fetching $username's $slug, putting in album $album"


    doGraphQL() {
    username=$1
    slug=$2
    query=$3
    headers=(
    -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0'
    -H 'Content-Type: application/json'
    -H 'Cookie: mx_t=c5cbc44f_b2dd_4e29_afba_a2c9d9d6ed03; csrftoken=4VDmUTpAWggOM3xzMG8Ltx32glJE9kSg; ch=AhcwEy6RKoCd9Dg0Q4PUUj28STBYGAMBmW1E6jFLDMk; c="gAAAAABnVAZ6gC4sfT1vgYcsVh7JAZEsABUXW7TaOv4SlbZXPdyyZmBYriuDBcxCl_EvUUu_0bOc8KySKG5GNqmtUCwo-jq2Ycw3x7fGseN2_wb7GW5QB0VLhqPAdQMcEAOH8cJ1xXLFYVEz8u248zZKKV3F6NmqABHx5eJKFrUTJyKYVCYMn_k="'
    )
    curl -s 'https://app.mixcloud.com/graphql' \
    -X POST \
    "${headers[@]}" \
    --data-raw "$( jq -cn \
    --arg slug "$slug" \
    --arg username "$username" \
    --arg query "$query" \
    '{id: "something", query: $query, variables: { lookup: { username: $username, slug: $slug}}}' )"
    }

    fetchSongList() {
    username=$1
    slug=$2
    #shellcheck disable=SC2016
    doGraphQL "$username" "$slug" \
    'query TracklistAudioPageQuery(
    $lookup: CloudcastLookup!
    ) {
    cloudcast: cloudcastLookup(lookup: $lookup) {
    canShowTracklist
    featuringArtistList
    moreFeaturingArtists
    sections {
    __typename
    ... on TrackSection {
    __typename
    artistName
    songName
    }
    ... on ChapterSection {
    chapter
    }
    ... on Node {
    __isNode: __typename
    id
    }
    }
    id
    }
    }' \
    | jq '[.data.cloudcast.sections[] | select(.artistName != null) | {artist: .artistName, song: .songName}]'
    }


    fetchDescriptionAndPicture() {
    username=$1
    slug=$2
    #shellcheck disable=SC2016
    doGraphQL "$username" "$slug" \
    'query cloudcastQuery(
    $lookup: CloudcastLookup!
    ) {
    cloudcast: cloudcastLookup(lookup: $lookup) {
    tags {
    tag {
    slug
    }
    }
    name
    description
    publishDate
    picture {
    urlRoot
    }
    }
    }
    ' \
    | jq '.data.cloudcast | {tags: [.tags[] | .tag.slug], publishDate: .publishDate, name: .name, description: .description, picture: "https://thumbnailer.mixcloud.com/unsafe/666x666/\(.picture.urlRoot)"}'
    }

    songList=$(fetchSongList "$username" "$slug" | tee /tmp/songList)
    meta=$(fetchDescriptionAndPicture "$username" "$slug" | tee /tmp/meta)
    #songList=$(cat /tmp/songList)
    #meta=$(cat /tmp/meta)

    description="$(echo "$meta" | jq -r '.description')"

    if [[ "$(echo "$songList" | jq 'length')" -gt 0 ]]; then
    description+="
    Incomplete Song List:
    $(echo "$songList" | jq -r '.[] | "\(.artist) - \(.song)"')"
    fi
    title="$(echo "$meta" | jq -r '.name')"
    image=$title.jpg
    songFile="$title.mp3"

    if [[ ! -f "$image" ]]; then
    curl -s "$(echo "$meta" | jq -r '.picture')" > "$image"
    fi

    eyeD3Args=(
    --to-v2.4
    --remove-all
    --artist="$(echo "$songList" | jq --arg artist "$username" -r '[{artist: $artist}] + . | [.[] | .artist] | join(", ")')"
    --album-artist="$username"
    --album="$album"
    --title="$title"
    --comment="$description"
    --recording-date="$(date -d "$(echo "$meta" | jq -r '.publishDate')" +"%Y-%m-%d")"
    --genre="$(echo "$meta" | jq -r '.tags | join("/")')"
    --add-image="$image:OTHER:from mixcloud"
    )

    if [[ ! -f "$songFile" ]]; then
    yt-dlp -x --audio-format mp3 --audio-quality 0 -o "$title.%(ext)s" "$url"
    fi

    eyeD3 "${eyeD3Args[@]}" "$songFile"