main 542324fb82f1 cached
133 files
2.7 MB
726.3k tokens
1 requests
Download .txt
Showing preview only (2,906K chars total). Download the full file or copy to clipboard to get everything.
Repository: adamint/spotify-web-api-kotlin
Branch: main
Commit: 542324fb82f1
Files: 133
Total size: 2.7 MB

Directory structure:
gitextract_6wsxvdzq/

├── .github/
│   ├── .github/
│   │   └── FUNDING.yml
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── ci-client.yml
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── README_ANDROID.md
├── TESTING.md
├── build.gradle.kts
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── publish_all.sh
├── settings.gradle.kts
├── src/
│   ├── androidMain/
│   │   ├── AndroidManifest.xml
│   │   ├── kotlin/
│   │   │   └── com/
│   │   │       └── adamratzman/
│   │   │           └── spotify/
│   │   │               ├── auth/
│   │   │               │   ├── SpotifyDefaultCredentialStore.kt
│   │   │               │   ├── implicit/
│   │   │               │   │   ├── AbstractSpotifyAppCompatImplicitLoginActivity.kt
│   │   │               │   │   ├── AbstractSpotifyAppImplicitLoginActivity.kt
│   │   │               │   │   ├── ImplicitAuthUtils.kt
│   │   │               │   │   └── SpotifyImplicitLoginActivity.kt
│   │   │               │   └── pkce/
│   │   │               │       ├── AbstractSpotifyPkceLoginActivity.kt
│   │   │               │       └── PkceAuthUtils.kt
│   │   │               ├── notifications/
│   │   │               │   ├── AbstractSpotifyBroadcastReceiver.kt
│   │   │               │   └── SpotifyBroadcastReceiverUtils.kt
│   │   │               └── utils/
│   │   │                   └── PlatformUtils.kt
│   │   └── res/
│   │       └── layout/
│   │           └── spotify_pkce_auth_layout.xml
│   ├── commonJvmLikeMain/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   ├── javainterop/
│   │                   │   └── SpotifyContinuation.kt
│   │                   └── utils/
│   │                       └── DateTimeUtils.kt
│   ├── commonJvmLikeTest/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   └── CommonImpl.kt
│   ├── commonMain/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify/
│   │           ├── SpotifyApi.kt
│   │           ├── SpotifyApiBuilder.kt
│   │           ├── SpotifyException.kt
│   │           ├── SpotifyRestAction.kt
│   │           ├── SpotifyScope.kt
│   │           ├── annotations/
│   │           │   └── ExperimentalAnnotations.kt
│   │           ├── endpoints/
│   │           │   ├── client/
│   │           │   │   ├── ClientEpisodeApi.kt
│   │           │   │   ├── ClientFollowingApi.kt
│   │           │   │   ├── ClientLibraryApi.kt
│   │           │   │   ├── ClientPersonalizationApi.kt
│   │           │   │   ├── ClientPlayerApi.kt
│   │           │   │   ├── ClientPlaylistApi.kt
│   │           │   │   ├── ClientProfileApi.kt
│   │           │   │   └── ClientShowApi.kt
│   │           │   └── pub/
│   │           │       ├── AlbumApi.kt
│   │           │       ├── ArtistApi.kt
│   │           │       ├── BrowseApi.kt
│   │           │       ├── EpisodeApi.kt
│   │           │       ├── FollowingApi.kt
│   │           │       ├── MarketsApi.kt
│   │           │       ├── PlaylistApi.kt
│   │           │       ├── SearchApi.kt
│   │           │       ├── ShowApi.kt
│   │           │       ├── TrackApi.kt
│   │           │       └── UserApi.kt
│   │           ├── http/
│   │           │   ├── Endpoints.kt
│   │           │   └── HttpRequest.kt
│   │           ├── models/
│   │           │   ├── Albums.kt
│   │           │   ├── Artists.kt
│   │           │   ├── Authentication.kt
│   │           │   ├── Browse.kt
│   │           │   ├── Episode.kt
│   │           │   ├── Library.kt
│   │           │   ├── LocalTracks.kt
│   │           │   ├── Misc.kt
│   │           │   ├── PagingObjects.kt
│   │           │   ├── Playable.kt
│   │           │   ├── Player.kt
│   │           │   ├── Playlist.kt
│   │           │   ├── ResultObjects.kt
│   │           │   ├── Show.kt
│   │           │   ├── SpotifySearchResult.kt
│   │           │   ├── SpotifyUris.kt
│   │           │   ├── Track.kt
│   │           │   ├── Users.kt
│   │           │   └── serialization/
│   │           │       └── SerializationUtils.kt
│   │           └── utils/
│   │               ├── ConcurrentHashMap.kt
│   │               ├── Encoding.kt
│   │               ├── ExternalUrls.kt
│   │               ├── IO.kt
│   │               ├── Language.kt
│   │               ├── Locale.kt
│   │               ├── Market.kt
│   │               ├── Platform.kt
│   │               ├── TimeUnit.kt
│   │               └── Utils.kt
│   ├── commonNonJvmTargetsTest/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify/
│   │           └── CommonImpl.kt
│   ├── commonTest/
│   │   ├── kotlin/
│   │   │   └── com.adamratzman/
│   │   │       └── spotify/
│   │   │           ├── AbstractTest.kt
│   │   │           ├── Common.kt
│   │   │           ├── priv/
│   │   │           │   ├── ClientEpisodeApiTest.kt
│   │   │           │   ├── ClientFollowingApiTest.kt
│   │   │           │   ├── ClientLibraryApiTest.kt
│   │   │           │   ├── ClientPersonalizationApiTest.kt
│   │   │           │   ├── ClientPlayerApiTest.kt
│   │   │           │   ├── ClientPlaylistApiTest.kt
│   │   │           │   └── ClientUserApiTest.kt
│   │   │           ├── pub/
│   │   │           │   ├── BrowseApiTest.kt
│   │   │           │   ├── EpisodeApiTest.kt
│   │   │           │   ├── MarketsApiTest.kt
│   │   │           │   ├── PublicAlbumsApiTest.kt
│   │   │           │   ├── PublicArtistsApiTest.kt
│   │   │           │   ├── PublicFollowingApiTest.kt
│   │   │           │   ├── PublicPlaylistsApiTest.kt
│   │   │           │   ├── PublicTracksApiTest.kt
│   │   │           │   ├── PublicUserApiTest.kt
│   │   │           │   ├── SearchApiTest.kt
│   │   │           │   └── ShowApiTest.kt
│   │   │           └── utilities/
│   │   │               ├── JsonTests.kt
│   │   │               ├── RestTests.kt
│   │   │               ├── UrisTests.kt
│   │   │               └── UtilityTests.kt
│   │   └── resources/
│   │       └── cached_responses.json
│   ├── desktopMain/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   └── utils/
│   │                       └── PlatformUtils.kt
│   ├── iosMain/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify.utils/
│   │           └── PlatformUtils.kt
│   ├── jsMain/
│   │   └── kotlin/
│   │       ├── co.scdn.sdk/
│   │       │   └── SpotifyPlayerJs.kt
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   ├── utils/
│   │                   │   ├── ImplicitGrant.kt
│   │                   │   └── PlatformUtils.kt
│   │                   └── webplayer/
│   │                       └── WebPlaybackSdk.kt
│   ├── jvmMain/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   └── utils/
│   │                       └── PlatformUtils.kt
│   ├── jvmTest/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   └── PkceTest.kt
│   ├── linuxX64Main/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify.utils/
│   │           └── PlatformUtils.kt
│   ├── macosX64Main/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify.utils/
│   │           └── PlatformUtils.kt
│   ├── mingwX64Main/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify.utils/
│   │           └── PlatformUtils.kt
│   ├── nativeDarwinMain/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   └── utils/
│   │                       └── PlatformUtils.kt
│   └── tvosMain/
│       └── kotlin/
│           └── com.adamratzman.spotify.utils/
│               └── PlatformUtils.kt
├── webpack.config.d/
│   └── patch.js
└── webpack.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/.github/FUNDING.yml
================================================
# These are supported funding model platforms

github: adamint # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: adamratzman # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: adamint # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: adamratzman # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/workflows/ci-client.yml
================================================
name: CI Client Test Workflow
on:
  workflow_dispatch:
    inputs:
      spotify_test_client_token:
        description: 'Spotify client redirect token (for client tests before release)'
        required: true
      spotify_test_redirect_uri:
        description: 'Spotify redirect uri'
        required: true
env:
  SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }}
  SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
  SPOTIFY_TOKEN_STRING: ${{ github.event.inputs.spotify_test_client_token }}
  SPOTIFY_REDIRECT_URI: ${{ github.event.inputs.spotify_test_redirect_uri }}
jobs:
  verify_client_android_jvm_linux_js:
    runs-on: ubuntu-latest
    environment: release
    steps:
      - name: Check out repo
        uses: actions/checkout@v2
      - name: Install java 11
        uses: actions/setup-java@v2
        with:
          distribution: 'adopt'
          java-version: '17'
      - name: Install curl
        run: sudo apt-get install -y curl libcurl4-openssl-dev
      - name: Verify Android
        run: ./gradlew testDebugUnitTest
      - name: Verify JVM/JS
        run: ./gradlew jvmTest
      - name: Archive test results
        uses: actions/upload-artifact@v2
        with:
          name: code-coverage-report
          path: build/reports
        if: always()

================================================
FILE: .github/workflows/ci.yml
================================================
name: CI Test Workflow

on:
  push:
    branches: [ main, dev, dev/** ]
  pull_request:
    branches: [ main, dev, dev/** ]

jobs:
  test_android_jvm_linux_trusted:
    runs-on: ubuntu-latest
    environment: testing
    env:
      SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }}
      SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
    steps:
    - name: Check out repo
      uses: actions/checkout@v2
    - name: Install java 11
      uses: actions/setup-java@v2
      with:
        distribution: 'adopt'
        java-version: '17'
    - name: Install curl
      run: sudo apt-get install -y curl libcurl4-openssl-dev
    - name: Test android
      run: ./gradlew testDebugUnitTest
    - name: Test jvm
      run: ./gradlew jvmTest
    - name: Archive test results
      uses: actions/upload-artifact@v2
      if: always()
      with:
        name: code-coverage-report
        path: build/reports


================================================
FILE: .github/workflows/release.yml
================================================
name: Deployment workflow
on:
  workflow_dispatch:
    inputs:
      release_version:
        description: 'Semantic version number to release'
        required: true
      spotify_test_client_token:
        description: 'Spotify client redirect token (for client tests before release)'
        required: true
      spotify_test_redirect_uri:
        description: 'Spotify redirect uri'
        required: true
env:
  SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }}
  SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
  # TODO temporarily deactivating client tests due to flaky (from spotify responses) tests
  #SPOTIFY_TOKEN_STRING: ${{ github.event.inputs.spotify_test_client_token }}
  SPOTIFY_REDIRECT_URI: ${{ github.event.inputs.spotify_test_redirect_uri }}
  NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
  NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
  ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
  ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
  SPOTIFY_API_PUBLISH_VERSION: ${{ github.event.inputs.release_version }}
jobs:
  release_android_jvm_linux_js:
    runs-on: ubuntu-latest
    environment: release
    steps:
      - name: Check out repo
        uses: actions/checkout@v2
      - name: Install java 11
        uses: actions/setup-java@v2
        with:
          distribution: 'adopt'
          java-version: '17'
      - name: Install curl
        run: sudo apt-get install -y curl libcurl4-openssl-dev
      - name: Verify Android
        run: ./gradlew testDebugUnitTest
      - name: Verify JVM/JS
        run: ./gradlew jvmTest
      - name: Publish JVM/Linux/Android
        run: ./gradlew publishKotlinMultiplatformPublicationToNexusRepository publishJvmPublicationToNexusRepository publishAndroidPublicationToNexusRepository publishLinuxX64PublicationToNexusRepository publishJsPublicationToNexusRepository
      - name: Archive test results
        uses: actions/upload-artifact@v2
        with:
          name: code-coverage-report
          path: build/reports
        if: always()
  release_mac:
    runs-on: macos-latest
    environment: release
    needs: release_android_jvm_linux_js
    steps:
      - name: Check out repo
        uses: actions/checkout@v2
      - name: Install java 11
        uses: actions/setup-java@v2
        with:
          distribution: 'adopt'
          java-version: '17'
      - name: Publish macOS/iOS
        run: ./gradlew publishMacosX64PublicationToNexusRepository publishIosX64PublicationToNexusRepository publishIosArm64PublicationToNexusRepository
  release_windows:
    runs-on: windows-latest
    environment: release
    needs: release_android_jvm_linux_js
    steps:
      - name: Check out repo
        uses: actions/checkout@v2
      - name: Install java 11
        uses: actions/setup-java@v2
        with:
          distribution: 'adopt'
          java-version: '17'
      - run: choco install curl
      - name: Publish windows
        run: ./gradlew publishMingwX64PublicationToNexusRepository
  release_docs:
    runs-on: ubuntu-latest
    environment: release
    steps:
      - name: Check out repo
        uses: actions/checkout@v2
      - name: Install java 11
        uses: actions/setup-java@v2
        with:
          distribution: 'adopt'
          java-version: '17'
      - name: Build docs
        run: ./gradlew dokkaHtml
      - name: Push docs to docs repo
        uses: cpina/github-action-push-to-another-repository@main
        env:
          API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }}
        with:
          source-directory: 'docs'
          destination-github-username: 'adamint'
          destination-repository-name: 'spotify-web-api-kotlin-docs'
          user-email: adam@adamratzman.com
          target-branch: main


================================================
FILE: .gitignore
================================================

.DS_STORE
# Created by https://www.toptal.com/developers/gitignore/api/gradle,kotlin,android,intellij+all,node
# Edit at https://www.toptal.com/developers/gitignore?templates=gradle,kotlin,android,intellij+all,node

### Android ###
# Built application files
*.apk
*.aar
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/
#  Uncomment the following line in case you need and you don't have the release build type files in your app
# release/

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

# Android Studio captures folder
captures/

# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml

# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/

# Google Services (e.g. APIs or Firebase)
# google-services.json

# Freeline
freeline.py
freeline/
freeline_project_description.json

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

# Version control
vcs.xml

# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/

### Android Patch ###
gen-external-apklibs
output.json

# Replacement of .externalNativeBuild directories introduced
# with Android Studio 3.5.

### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn.  Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### Intellij+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360

.idea/

# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023

modules.xml
.idea/misc.xml
*.ipr

# Sonarlint plugin
.idea/sonarlint

### Kotlin ###
# Compiled class file

# Log file

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

### Node ###
# Logs
logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

### Gradle ###
.gradle

# Ignore Gradle GUI config
gradle-app.setting

# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

# Cache of project
.gradletasknamecache

# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties

### Gradle Patch ###
**/build/

# End of https://www.toptal.com/developers/gitignore/api/gradle,kotlin,android,intellij+all,node
docs/

================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

When contributing to this repository, feel free to first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.

However, any library additions are always welcome. I am especially looking for the addition of new Kotlin/Native 
targets.

## Testing
Please see [testing.md](TESTING.md) for full testing instructions. Your contributions should be able to pass every test.

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2017 Adam Ratzman

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Kotlin Spotify Web API 
A [Kotlin](https://kotlinlang.org/) implementation of the [Spotify Web API](https://developer.spotify.com/web-api/),
supporting Kotlin/JS, Kotlin/Android, Kotlin/JVM, and Kotlin/Native
(macOS, Windows, Linux).

This library has first-class support for Java and is a viable alternative for Java development. 
Please see [the Java section](#java) for more details.

**Use this library in Kotlin, Java, JavaScript, Swift, or native code!** Because this library targets both iOS and Android, it can also be used in KMM ([Kotlin Multiplatform Mobile](https://kotlinlang.org/lp/mobile/)) applications as a shared source.

[![Maven CEntral](https://maven-badges.herokuapp.com/maven-central/com.adamratzman/spotify-api-kotlin-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.adamratzman/spotify-api-kotlin-core)
[![](https://img.shields.io/badge/Documentation-latest-orange.svg)](https://adamint.github.io/spotify-web-api-kotlin-docs/)
![](https://img.shields.io/badge/License-MIT-blue.svg)
[![codebeat badge](https://codebeat.co/badges/0ab613b0-31d7-4848-aebc-4ed1e51f069c)](https://codebeat.co/projects/github-com-adamint-spotify-web-api-kotlin-master)

## Table of Contents
* [Oerview and how to install](#overview-and-how-to-install)
    + [JVM, Android, JS, Native](#jvm-android-js-native-apple)
    + [Android information](#android)
      * [Android sample app](#android-sample-application)
* [Documentation](#documentation)
* [Need help, have a question, or want to contribute?](#have-a-question)
* [Creating a new api instance](#creating-a-new-api-instance)
    + [SpotifyAppApi](#spotifyappapi)
    + [SpotifyClientApi](#spotifyclientapi)
        * [PKCE](#pkce)
        * [Non-PKCE](#non-pkce-backend-applications-requires-client-secret)
    + [SpotifyImplicitGrantApi](#spotifyimplicitgrantapi)
    + [SpotifyApiBuilder block & setting API options](#spotifyapibuilder-block--setting-api-options)
        * [API options](#api-options)
    + [Using the API](#using-the-api)
* [Platform-specific wrappers and information](#platform-specific-wrappers-and-information)
    + [Java](#java)
    + [Android authentication](#android-authentication)
    + [JavaScript: Spotify Web Playback SDK wrapper](#js-spotify-web-playback-sdk-wrapper)
* [Tips](#tips)
    + [Building the API](#building-the-api)
* [Notes](#notes)
    + [LinkedResults, PagingObjects, and Cursor-based Paging Objects](#the-benefits-of-linkedresults-pagingobjects-and-cursor-based-paging-objects)
    + [Generic Requests](#generic-request)
    + [Track Relinking](#track-relinking)
* [Contributing](#contributing)

## Overview and how to install
Current version:

[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.adamratzman/spotify-api-kotlin-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.adamratzman/spotify-api-kotlin-core)

### JVM, Android, JS, Native, Apple
```
repositories {
    mavenCentral()
}

implementation("com.adamratzman:spotify-api-kotlin-core:VERSION")
```


### JS
Please see the [JS Spotify Web Playback SDK wrapper](#js-spotify-web-playback-sdk-wrapper) to learn how to use Spotify's web playback SDK in a browser application.

### Android
**Note**: For information on how to integrate implicit/PKCE authentication, Spotify app remote, and Spotify broadcast notifications into 
your application, please see the [Android README](README_ANDROID.md).


*If you declare any release types not named debug or release, you may see "Could not resolve com.adamratzman:spotify-api-kotlin-android:VERSION". You need to do the following for each release type not named debug or release:*
```
android {
    buildTypes {
        yourReleaseType1 {
            // ...
            matchingFallbacks = ['release', 'debug'] 
        }
        yourReleaseType2 {
            // ...
            matchingFallbacks = ['release', 'debug'] 
        }
	...
    }
}
```


To successfully build, you might need to exclude kotlin_module files from the packaging. To do this, inside the android/buildTypes/release closure, you would put:
```
packagingOptions {
	exclude 'META-INF/*.kotlin_module'
}
```

#### Android sample application
You can find a simple sample application demonstrating how to use spotify-web-api-kotlin in a modern Android app, as well as how to integrate with the Spotify app, [here](https://github.com/Nielssg/Spotify-Api-Test-App).

## Documentation
The `spotify-web-api-kotlin` JavaDocs are hosted [here](https://adamint.github.io/spotify-web-api-kotlin-docs/).

## Have a question?
If you have a question, you can:

1. Create an [issue](https://github.com/adamint/spotify-web-api-kotlin/issues)
2. Join our [Discord server](https://discord.gg/G6vqP3S)
3. Contact me using **Adam#9261** on [Discord](https://discordapp.com)

## Unsupported features on each platform:
| Feature                     | JVM                | Android            | JS                 | Native (Mac/Windows/Linux) |
|-----------------------------|--------------------|--------------------|--------------------|----------------------------|
| Edit client playlist        | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | Unsupported                |
| Remove playlist tracks      | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | Unsupported                |

Please feel free to open an issue/discussion on GitHub or Discord if you need access to one of these features 
or have an interest in implementing one, as direction can be provided.

## Creating a new api instance
To decide which api you need (SpotifyAppApi, SpotifyClientApi, SpotifyImplicitGrantApi), you can refer 
to the sections below or the [Spotify authorization guide](https://developer.spotify.com/documentation/general/guides/authorization-guide/). In general:
- If you don't need client resources, use SpotifyAppApi
- If you're using the api in a backend application, use SpotifyClientApi (with or without PKCE)
- If you're using the api in Kotlin/JS browser, use SpotifyImplicitGrantApi
- If you need access to client resources in an Android or other application, use SpotifyClientApi with PKCE

**Note**: You can use the online [Spotify OAuth Token Generator](https://adamratzman.com/projects/spotify/generate-token) tool to generate a client token for local testing.

### SpotifyAppApi
This provides access only to public Spotify endpoints.
Use this when you have a server-side application. Note that implicit grant authorization 
provides a higher api ratelimit, so consider using implicit grant if your application has 
significant usage.

By default, the SpotifyApi `Token` automatically regenerates when needed. 
This can be changed by overriding the `automaticRefresh` builder setting.

There are four exposed builders, depending on the level of control you need over api creation. 
Please see the [spotifyAppApi builder docs](https://adamint.github.io/spotify-web-api-kotlin-docs/spotify-web-api-kotlin/com.adamratzman.spotify/-spotify-app-api/index.html) for a full list of available builders.

You will need:
- Spotify application client id
- Spotify application client secret

Example creation (default settings)

```kotlin
val api = spotifyAppApi("clientId", "clientSecret").build() // create and build api
println(api.browse.getNewReleases()) // use it
```

Example creation, using an existing Token and setting automatic token refresh to false
```kotlin
val token = spotifyAppApi(spotifyClientId, spotifyClientSecret).build().token
val api = spotifyAppApi(
  "clientId",
  "clientSecret",
  token
) { 
  automaticRefresh = false 
}.build()

println(api.browse.getNewReleases()) // use it
```

### SpotifyClientApi
The `SpotifyClientApi` is a superset of `SpotifyApi`; thus, nothing changes if you want to 
access public data.
This library does not provide a method to retrieve the code from your  callback url; instead,
you must implement that with a web server. 
Automatic Token refresh is available *only* when building with an authorization code or a 
`Token` object. Otherwise, it will expire `Token.expiresIn` seconds after creation.

Make sure your application has requested the proper [Scopes](https://developer.spotify.com/web-api/using-spotifyScopes/) in order to 
ensure proper function of this library. The api option `requiredScopes` allows you to verify 
that a client has actually authorized with the scopes you are expecting.

You will need:
- Spotify application client id
- Spotify application client secret (if not using PKCE)
- Spotify application redirect uri
- To choose which client authorization method (PKCE or non-PKCE) to use

#### PKCE
Use the PKCE builders and helper methods if you are using the Spotify client authorization PKCE flow.
Building via PKCE returns a `SpotifyClientApi` which has modified refresh logic.

Use cases:
1. You are using this library in an application (likely Android), or do not want to expose the client secret.

To learn more about the PKCE flow, please read the [Spotify authorization guide](https://developer.spotify.com/documentation/general/guides/authorization-guide/#implicit-grant-flow).
Some highlights about the flow are:
- It is refreshable, but each refresh token can only be used once. This library handles token refresh automatically by default
- It does not require a client secret; instead, a set redirect uri and a random code verifier 
are used to verify the authenticity of the authorization.
- A code verifier is required. The code verifier is "*a cryptographically random string between 43 and 128 characters in length. 
It can contain letters, digits, underscores, periods, hyphens, or tildes.*"
- A code challenge is required. "*In order to generate the code challenge, your app should 
hash the code verifier using the SHA256 algorithm. Then, base64url encode the hash that you generated.*"
- When creating a pkce api instance, the code verifier is passed in by you and compared to 
the code challenge used to authorize the user.

This library contains helpful methods that can be used to simplify the PKCE authorization process.
This includes `getSpotifyPkceCodeChallenge`, which SHA256 hashes and base64url encodes the code 
challenge, and `getSpotifyPkceAuthorizationUrl`, which allows you to generate an easy authorization url for PKCE flow.

Please see the [spotifyClientPkceApi builder docs](https://adamint.github.io/spotify-web-api-kotlin-docs/spotify-web-api-kotlin/com.adamratzman.spotify/spotify-client-pkce-api.html) for a full list of available builders.
 
**Takeaway**: Use PKCE authorization flow in applications where you cannot secure the client secret.

To get a PKCE authorization url, to which you can redirect a user, you can use the `getSpotifyPkceAuthorizationUrl`
top-level method. An example is shown below, requesting 4 different scopes.
```kotlin
val codeVerifier = "thisisaveryrandomalphanumericcodeverifierandisgreaterthan43characters"
val codeChallenge = getSpotifyPkceCodeChallenge(codeVerifier) // helper method
val url: String = getSpotifyPkceAuthorizationUrl(
    SpotifyScope.PLAYLIST_READ_PRIVATE,
    SpotifyScope.PLAYLIST_MODIFY_PRIVATE,
    SpotifyScope.USER_FOLLOW_READ,
    SpotifyScope.USER_LIBRARY_MODIFY,
    clientId = "clientId",
    redirectUri = "your-redirect-uri",
    codeChallenge = codeChallenge
)
```

There is also an optional parameter `state`, which helps you verify the authorization.

**Note**: If you want automatic token refresh, you need to pass in your application client id and redirect uri 
when using the `spotifyClientPkceApi`.

##### Example: A user has authorized your application. You now have the authorization code obtained after the user was redirected back to your application. You want to create a new `SpotifyClientApi`.
```kotlin
val codeVerifier = "thisisaveryrandomalphanumericcodeverifierandisgreaterthan43characters"
val code: String = ...
val api = spotifyClientPkceApi(
    "clientId", // optional. include for token refresh
    "your-redirect-uri", // optional. include for token refresh
    code,
    codeVerifier // the same code verifier you used to generate the code challenge
) {
  retryWhenRateLimited = false
}.build()

println(api.library.getSavedTracks().take(10).filterNotNull().map { it.track.name })
```

#### Non-PKCE (backend applications, requires client secret)
To get a non-PKCE authorization url, to which you can redirect a user, you can use the `getSpotifyAuthorizationUrl`
top-level method. An example is shown below, requesting 4 different scopes.
```kotlin
val url: String = getSpotifyAuthorizationUrl(
    SpotifyScope.PLAYLIST_READ_PRIVATE,
    SpotifyScope.PLAYLIST_MODIFY_PRIVATE,
    SpotifyScope.USER_FOLLOW_READ,
    SpotifyScope.USER_LIBRARY_MODIFY,
    clientId = "clientId",
    redirectUri = "your-redirect-uri",
    state = "your-special-state" // optional
)
```
There are also several optional parameters, allowing you to set whether the authorization url is meant 
for implicit grant flow, the state, and whether a re-authorization dialog should be shown to users.

There are several exposed builders, depending on the level of control you need over api creation. 
Please see the [spotifyClientApi builder docs](https://adamint.github.io/spotify-web-api-kotlin-docs/spotify-web-api-kotlin/com.adamratzman.spotify/spotify-client-api.html) for a full list of available builders.

##### Example: You've redirected the user back to your web server and have an authorization code (code).
In this example, automatic token refresh is turned on by default.
```kotlin
val authCode = ""
val api = spotifyClientApi(
    "clientId",
    "clientSecret",
    "your-redirect-uri",
    authCode
).build() // create and build api
println(api.personalization.getTopTracks(limit = 5).items.map { it.name }) // print user top tracks
```

##### Example: You've saved a user's token from previous authorization and need to create an api instance.
In this case, if you provide a client id to the builder, automatic token refresh will also be turned on.
```kotlin
val token: Token = ... // your existing token
val api = spotifyClientApi(
    "clientId",
    "clientSecret",
    "your-redirect-uri",
    token
) {
  onTokenRefresh = {
    println("Token refreshed at ${System.currentTimeMillis()}")
  }
}.build()
println(api.personalization.getTopTracks(limit = 5).items.map { it.name })
```


### SpotifyImplicitGrantApi
Use the `SpotifyImplicitGrantApi` if you are using the Spotify implicit grant flow.
`SpotifyImplicitGrantApi` is a superset of `SpotifyClientApi`.
Unlike the other builders, the `spotifyImplicitGrantApi` builder method directly returns 
a `SpotifyImplicitGrantApi` instead of an api builder.

Use cases:
1. You are using the **Kotlin/JS** target for this library.
2. Your frontend Javascript passes the token received through the implicit grant flow to your 
backend, where it is then used to create an api instance.

To learn more about the implicit grant flow, please read the [Spotify authorization guide](https://developer.spotify.com/documentation/general/guides/authorization-guide/#implicit-grant-flow).
Some highlights about the flow are:
- It is non-refreshable
- It is client-side
- It does not require a client secret

Please see the [spotifyImplicitGrantApi builder docs](https://adamint.github.io/spotify-web-api-kotlin-docs/spotify-web-api-kotlin/com.adamratzman.spotify/spotify-implicit-grant-api.html) for a full list of available builders.
 
The Kotlin/JS target contains the `parseSpotifyCallbackHashToToken` method, which will parse the hash 
for the current url into a Token object, with which you can then instantiate the api.

**Takeaway**: There are two ways to use implicit grant flow, browser-side only and browser and 
server. This library provides easy access for both.

##### Example
```kotlin
val token: Token = ...
val api = spotifyImplicitGrantApi(
    null,
    null,
    token
) // create api. there is no need to build it 
println(api.personalization.getTopArtists(limit = 1)[0].name) // use it
```

### SpotifyApiBuilder Block & setting API options 
There are three pluggable blocks in each api's corresponding builder

1. `credentials` lets you set the client id, client secret, and redirect uri
2. `authorization` lets you set the type of api authorization you are using. 
Acceptable types include: an authorization code, a `Token` object, a Token's access code string, and an optional refresh token string
3. `options` lets you configure API options to your own specific needs

#### API options
This library does not attempt to be prescriptivist. 
All API options are located in `SpotifyApiOptions` and their default values can be overridden; however, use caution in doing so, as 
most of the default values either allow for significant performance or feature enhancements to the API instance.

- `useCache`: Set whether to cache requests. Default: true
- `cacheLimit`: The maximum amount of cached requests allowed at one time. Null means no limit. Default: 200
- `automaticRefresh`: Enable or disable automatic refresh of the Spotify access token when it expires. Default: true
- `retryWhenRateLimited`: Set whether to block the current thread and wait until the API can retry the request. Default: true
- `enableLogger`: Set whether to enable to the exception logger. Default: true
- `testTokenValidity`: After API creation, test whether the token is valid by performing a lightweight request. Default: false
- `defaultLimit`: The default amount of objects to retrieve in one request. Default: 50
- `json`: The Json serializer/deserializer instance.
- `allowBulkRequests`: Allow splitting too-large requests into smaller, allowable api requests. Default: true 
- `requestTimeoutMillis`: The maximum time, in milliseconds, before terminating an http request. Default: 100000ms
- `refreshTokenProducer`: Provide if you want to use your own logic when refreshing a Spotify token.
- `requiredScopes`: Scopes that your application requires to function (only applicable to `SpotifyClientApi` and `SpotifyImplicitGrantApi`).
This verifies that the token your user authorized with actually contains the scopes your 
application needs to function.

Notes:
- Unless you have a good reason otherwise, `useCache` should be true
- `cacheLimit` is per Endpoint, not per API. Don't be surprised if you end up with over 200 items in your cache with the default settings.
- `automaticRefresh` is disabled when client secret is not provided, or if tokenString is provided in SpotifyClientApi
- `allowBulkRequests` for example, lets you query 80 artists in one wrapper call by splitting it into 50 artists + 30 artists
- `refreshTokenProducer` is useful when you want to re-authorize with the Spotify Auth SDK or elsewhere

### Using the API
APIs available in all `SpotifyApi` instances, including `SpotifyClientApi` and `SpotifyImplicitGrantApi`:
- `SearchApi` (searching items)
- `AlbumApi` (get information about albums)
- `BrowseApi` (browse new releases, featured playlists, categories, and recommendations)
- `ArtistApi` (get information about artists)
- `PlaylistApi` (get information about playlists)
- `UserApi` (get public information about users on Spotify)
- `TrackApi` (get information about tracks)
- `FollowingApi` (check whether users follow playlists)

APIs available only in `SpotifyClientApi` and `SpotifyImplicitGrantApi` instances:
- `ClientSearchApi` (all the methods in `SearchApi`, and searching shows and episodes)
- `EpisodeApi` (get information about episodes)
- `ShowApi` (get information about shows)
- `ClientPlaylistApi` (all the methods in `PlaylistApi`, and get and manage user playlists)
- `ClientProfileApi` (all the methods in `UserApi`, and get the user profile, depending on scopes)
- `ClientFollowingApi` (all the methods in `FollowingApi`, and get and manage following of playlists, artists, and users)
- `ClientPersonalizationApi` (get user top tracks and artists)
- `ClientLibraryApi` (get and manage saved tracks and albums)
- `ClientPlayerApi` (view and control Spotify playback)

## Platform-specific wrappers and information

### Java
This library has first-class support for Java! You have two choices when using this library: async-only with Kotlin
suspend functions (using SpotifyContinuation).


#### Integrating with Kotlin suspend functions via Java `Continuation`s
Unfortunately, coroutines don't play very nicely with Java code. Fortunately, however, we provide a wrapper around Kotlin's 
`Continuation` class that allows you to directly implement `onSuccess` and `onFailure` handlers on API methods.

Please see below for an example:

```java
import com.adamratzman.spotify.SpotifyApiBuilderKt;
import com.adamratzman.spotify.SpotifyAppApi;
import com.adamratzman.spotify.javainterop.SpotifyContinuation;
import com.adamratzman.spotify.models.Album;
import org.jetbrains.annotations.NotNull;

import java.util.concurrent.ExecutionException;

public class SpotifyTestApp {
    static SpotifyAppApi api;

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        var id = "spotify-client-id";
        var secret = "spotify-client-secret";
        SpotifyApiBuilderKt.spotifyAppApi(id, secret).build(true, new SpotifyContinuation<>() {
            @Override
            public void onSuccess(SpotifyAppApi spotifyAppApi) {
                api = spotifyAppApi;
                runAlbumSearch();
            }

            @Override
            public void onFailure(@NotNull Throwable throwable) {
                throwable.printStackTrace();
            }
        });

        Thread.sleep(1000000);
    }

    public static void runAlbumSearch() {
        api.getAlbums().getAlbum("spotify:album:0b23AHutIA1BOW0u1dZ6wM", null, new SpotifyContinuation<>() {
            @Override
            public void onSuccess(Album album) {
                System.out.println("Album name is: " + album.getName() + ". Exiting now..");

                System.exit(0);
            }

            @Override
            public void onFailure(@NotNull Throwable throwable) {
                throwable.printStackTrace();
            }
        });
    }
}
```

### Android authentication
For information on how to integrate implicit/PKCE authentication, Spotify app remote, and Spotify broadcast notifications into 
your application, please see the [Android README](README_ANDROID.md).

### JS Spotify Web Playback SDK wrapper
`spotify-web-api-kotlin` provides a wrapper around Spotify's [Web Playback SDK](https://developer.spotify.com/documentation/web-playback-sdk/reference/) 
for playing music via Spotify in the browser on your own site.

To do this, you need to create a `Player` instance and then use the associated methods to register listeners, play, 
and get current context.

Please see an example of how to do this [here](https://github.com/adamint/spotify-web-api-browser-example/blob/95df60810611ddb961a7a2cb0c874a76d4471aa7/src/main/kotlin/com/adamratzman/layouts/HomePageComponent.kt#L38). 
An example project, [spotify-web-api-browser-example](https://github.com/adamint/spotify-web-api-browser-example), 
demonstrates how to create a frontend JS Kotlin application with Spotify integration and 
that will play music in the browser.

**Notes**:
1. You must include the Spotify player JS script by including `<script src="https://sdk.scdn.co/spotify-player.js"></script>`
2. You must define a `window.onSpotifyWebPlaybackSDKReady` function immediately afterwards - this should load your main application bundle.
    Otherwise, you will get errors. An example is below:
   
```html
<html>
<head>
    ...
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>

    <script>
        jQuery.loadScript = function (url, callback) {
            jQuery.ajax({
                url: url,
                dataType: 'script',
                success: callback,
                async: true
            });
        }
    </script>

    <script src="https://sdk.scdn.co/spotify-player.js"></script>
    <script>
        window.onSpotifyWebPlaybackSDKReady = () => {
            $.loadScript("main.bundle.js")
        }
    </script>
</head>
<body>
....
</body>
</html>
```

## Tips

### Building the API
The easiest way to build the API is using .build() after a builder
```kotlin
runBlocking {
    val api = spotifyAppApi(clientId, clientSecret).build()
}
```

## Notes
### Re-authentication
If you are using an authorization flow or token that does not support automatic token refresh, `SpotifyException.ReAuthenticationNeededException` 
will be thrown. You should put your requests, if creating an application, behind a try/catch block to re-authenticate users if this 
exception is thrown.

### LinkedResults, PagingObjects, and Cursor-based Paging Objects
Spotify provides these three object models in order to simplify our lives as developers. So let's see what we
can do with them!

#### PagingObjects
PagingObjects are a container for the requested objects (`items`), but also include 
important information useful in future calls. It contains the request's `limit` and `offset`, along with 
(sometimes) a link to the next and last page of items and the total number of items returned.

If a link to the next or previous page is provided, we can use the `getNext` and `getPrevious` methods to retrieve 
the respective PagingObjects 

#### Cursor-Based Paging Objects
A cursor-based paging object is a PagingObject with a cursor added on that can be used as a key to find the next 
page of items. The value in the cursor, `after`, describes after what object to begin the query.

Just like with PagingObjects, you can get the next page of items with `getNext`. *However*, there is no 
provided implementation of `after` in this library. You will need to do it yourself, if necessary.

#### LinkedResults
Some endpoints, like `PlaylistAPI.getPlaylistTracks`, return a LinkedResult, which is a simple wrapper around the 
list of objects. With this, we have access to its Spotify API url (with `href`), and we provide simple methods to parse 
that url.

### Generic Request
For obvious reasons, in most cases, making asynchronous requests via `queue` or `queueAfter` is preferred. However, 
the synchronous format is also shown.

```kotlin
val api = spotifyAppApi(
        System.getenv("SPOTIFY_CLIENT_ID"),
        System.getenv("SPOTIFY_CLIENT_SECRET")
).build()

// print out the names of the twenty most similar songs to the search
println(api.search.searchTrack("Début de la Suite").joinToString { it.name })

// simple, right? what about if we want to print out the featured playlists message from the "Overview" tab?
println(api.browse.getFeaturedPlaylists().message)

// easy! let's try something a little harder
// let's find out Bénabar's Spotify ID, find his top tracks, and print them out
val benabarId = api.search.searchArtist("Bénabar")[0].id
// this works; a redundant way would be: api.artists.getArtist("spotify:artist:6xoAWsIOZxJVPpo7Qvqaqv").id
println(api.artists.getArtistTopTracks(benabarId).joinToString { it.name })
```

### Track Relinking
Spotify keeps many instances of most tracks on their servers, available in different markets. As such, if we use endpoints 
that return tracks, we do not know if these tracks are playable in our market. That's where track relinking comes in.

To relink in a specified market, we must supply a `market` parameter for endpoints where available. 
In both Track and SimpleTrack objects in an endpoint response, there is a nullable field called `linked_from`. 
If the track is unable to be played in the specified market and there is an alternative that *is* playable, this 
will be populated with the href, uri, and, most importantly, the id of the track.

You can then use this track in `SpotifyClientApi` endpoints such as playing or saving the track, knowing that it will be playable 
in your market!

## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)


================================================
FILE: README_ANDROID.md
================================================
# spotify-web-api-kotlin Android target extended features

Please also read the Android section of the spotify-web-api-kotlin readme, as it contains details about how to support
non-debug/release release types.

spotify-web-api-kotlin contains wrappers around Spotify's `android-auth` and `android-sdk` (Spotify remote) libraries
that make it easier to implement authentication and playback features, while only needing to learn how to use one
library.

## Table of Contents

* [Sample application](#sample-application)
* [Authentication](#authentication)
    + [Credential store](#spotify-credential-store)
    + [Authentication prerequisites](#authentication-prerequisites)
    + [PKCE auth (refreshable client authentication)](#pkce-auth)
    + [spotify-auth integration (implicit auth)](#implicit-auth)
    + [Using SpotifyApi in your application](#using-spotifyapi-in-your-application)
* [spotify-remote integration (WIP)](#spotify-remote-integration)
* [Broadcast Notifications](#broadcast-notifications)
    + [JVM, Android, JS, Native](#jvm-android-js)
    + [Android information](#android)

## Sample application

There is a sample application demonstrating authentication, remote integration, and broadcast notifications. You may
find it useful to scaffold parts of your application, or just to learn more about the Android features in this library.
See https://github.com/Nielssg/Spotify-Api-Test-App

## Authentication

spotify-web-api-kotlin comes with two built-in authorization schemes. The library includes a full implementation of the
PKCE authorization scheme, which allows you to refresh the token you obtain indefinitely*, as well as wrapping around
the `spotify-auth` to provide a simple way to perform implicit grant authorization (non-refreshable tokens).

\* PKCE tokens are refreshable unless they are revoked. If they are revoked, requests will fail with error 400, and you
should begin authorization flow again.

### Spotify Credential Store

By default, credentials are stored in the `SpotifyDefaultCredentialStore`, which under-the-hood creates and updates
an `EncryptedSharedPreferences` instance.

#### Creating an instance of the credential store

```kotlin
 val credentialStore by lazy {
    SpotifyDefaultCredentialStore(
        clientId = "YOUR_SPOTIFY_CLIENT_ID",
        redirectUri = "YOUR_SPOTIFY_REDIRECT_URI",
        applicationContext = YOUR_APPLICATION.context
    )
}
```

It is recommended to maintain only one instance of the credential store.

#### Setting credentials

You can set credentials in several different ways. The first two are recommended, for simplicity.

1. You can pass an instance of `SpotifyApi` using `SpotifyDefaultCredentialStore.setSpotifyApi(api: GenericSpotifyApi)`.
   This will directly set the `token` property.
2. You can set the `token` property using `SpotifyDefaultCredentialStore.token = YOUR_TOKEN`. This will set all three
   saved properties, mentioned below.
3. You can set the `spotifyTokenExpiresAt`, `spotifyAccessToken`, and `spotifyRefreshToken` properties. Please note that
   all of them are used to create a `Token`, so failing to update any single property may result in unintended
   consequences.

Example:

```kotlin
val credentialStore = (application as MyApplication).model.credentialStore
credentialStore.setSpotifyApi(spotifyApi)
```

#### Getting an instance of SpotifyApi from the credential store

Based on the type of authorization you used to authenticate the user, you will either call `getSpotifyImplicitGrantApi`
or `getSpotifyClientPkceApi`. Both methods allow you to optionally pass parameters to configure the returned
`SpotifyApi`.

#### Saving credentials somewhere other than the credential store (TBD)

Unfortunately, you are only able to store credentials in the credential store at this time if you decide to use the
authentication features of this library. PRs are welcome to address this limitation.

### Authentication prerequisites

1. You
   must [register your application](https://developer.spotify.com/documentation/general/guides/app-settings/#register-your-app)
   on Spotify. You must specify at least one application redirect uri (such as myapp://myauthcallback) - you will need
   this later.
2. Though this is not required, for security reasons you should follow the **Register Your App** part of the Spotify
   Android guide listed
   [here](https://developer.spotify.com/documentation/android/quick-start/) to generate a fingerprint for your app.

**Note**: Ensure that you are not using the same redirect uri for both PKCE/implicit authorization. If you need to use
both, please register two distinct redirect uris.

### PKCE Auth

PKCE authorization lets you obtain a refreshable Spotify token. This means that you do not need to keep prompting your
users to re-authenticate (or force them to wait a second for automatic login). Please read the "PKCE" section of
the [README](README.md) if you'd like to learn more.

#### 1. Create a class implementing AbstractSpotifyPkceLoginActivity

You first need to create a class that extends AbstractSpotifyPkceLoginActivity that will be used for the actual user
authorization.

Example:

```kotlin
internal var pkceClassBackTo: Class<out Activity>? = null

class SpotifyPkceLoginActivityImpl : AbstractSpotifyPkceLoginActivity() {
    override val clientId = BuildConfig.SPOTIFY_CLIENT_ID
    override val redirectUri = BuildConfig.SPOTIFY_REDIRECT_URI_PKCE
    override val scopes = SpotifyScope.values().toList()

    override fun onSuccess(api: SpotifyClientApi) {
        val model = (application as SpotifyPlaygroundApplication).model
        model.credentialStore.setSpotifyApi(api)
        val classBackTo = pkceClassBackTo ?: ActionHomeActivity::class.java
        pkceClassBackTo = null
        toast("Authentication via PKCE has completed. Launching ${classBackTo.simpleName}..")
        startActivity(Intent(this, classBackTo))
    }

    override fun onFailure(exception: Exception) {
        exception.printStackTrace()
        pkceClassBackTo = null
        toast("Auth failed: ${exception.message}")
    }
}
```

#### 2. Add the following activity to your Android Manifest
Note: the protocol of your redirect uri corresponds to the Android scheme, and the path corresponds to the 
host. Ex: for the redirect uri `myapp://authcallback`, the scheme is `myapp` and the host is `authcallback`.

```xml

<application>
    ...
    <activity android:name="YOUR_CLASS_IMPLEMENTING_AbstractSpotifyPkceLoginActivity"
              android:launchMode="singleTop">
        <intent-filter>
            <action android:name="android.intent.action.VIEW"/>

            <category android:name="android.intent.category.DEFAULT"/>
            <category android:name="android.intent.category.BROWSABLE"/>

            <data android:scheme="YOUR_REDIRECT_SCHEME" android:host="YOUR_REDIRECT_HOST"/>
        </intent-filter>
    </activity>
</application>
```

#### 3. Begin Spotify authorization flow with Activity.startSpotifyClientPkceLoginActivity
Now, you just need to begin the authorization flow by calling `Activity.startSpotifyClientPkceLoginActivity` in any 
activity in your application.

Example:
```kotlin
pkceClassBackTo = classBackTo // from the previous code sample, return to an activity after auth success
startSpotifyClientPkceLoginActivity(YOUR_CLASS_IMPLEMENTING_PKCE_LOGIN_ACTIVITY::class.java)
```

#### 4. ???

#### 5. Profit (more accurately, your onSuccess or onFailure methods will be called)

### Implicit auth
Implicit grant authorization, provided by wrapping the `spotify-auth` library, returns a temporary, non-refreshable 
access token. Implementing this authorization method is very similar to PKCE authorization, as both follow the 
same general format.

#### 1. Create a class implementing AbstractSpotifyAppLoginActivity or AbstractSpotifyAppCompatImplicitLoginActivity.
You first need to create a class that extends `AbstractSpotifyAppLoginActivity` or `AbstractSpotifyAppCompatImplicitLoginActivity` 
that will be used for the actual user authorization. The only difference between these two classes is that 
`AbstractSpotifyAppLoginActivity` extends from `Activity`, while `AbstractSpotifyAppCompatImplicitLoginActivity` extends 
from `AppCompatActivity`.

Example:

```kotlin
class SpotifyImplicitLoginActivityImpl : AbstractSpotifyAppImplicitLoginActivity() {
    override val state: Int = 1337
    override val clientId: String = BuildConfig.SPOTIFY_CLIENT_ID
    override val redirectUri: String = BuildConfig.SPOTIFY_REDIRECT_URI_AUTH
    override val useDefaultRedirectHandler: Boolean = false
    override fun getRequestingScopes(): List<SpotifyScope> = SpotifyScope.values().toList()

    override fun onSuccess(spotifyApi: SpotifyImplicitGrantApi) {
        val model = (application as SpotifyPlaygroundApplication).model
        model.credentialStore.setSpotifyApi(spotifyApi)
        toast("Authentication via spotify-auth has completed. Launching TrackViewActivity..")
        startActivity(Intent(this, ActionHomeActivity::class.java))
    }

    override fun onFailure(errorMessage: String) {
        toast("Auth failed: $errorMessage")
    }
}
```

#### 2. Add the following activity to your Android Manifest
Note: the protocol of your redirect uri corresponds to the Android scheme, and the path corresponds to the
host. Ex: for the redirect uri `myapp://authcallback`, the scheme is `myapp` and the host is `authcallback`.

```xml

<application>
    ...
    <activity
            android:name="YOUR_CLASS_IMPLEMENTING_AbstractSpotifyAppImplicitLoginActivity"
            android:theme="@android:style/Theme.Translucent.NoTitleBar">
        <intent-filter>
            <action android:name="android.intent.action.VIEW"/>
            <category android:name="android.intent.category.DEFAULT"/>
            <category android:name="android.intent.category.BROWSABLE"/>
            <data android:scheme="YOUR_REDIRECT_SCHEME" android:host="YOUR_REDIRECT_HOST"/>
        </intent-filter>
    </activity>

    <activity
            android:name="com.spotify.sdk.android.auth.LoginActivity"
            android:theme="@android:style/Theme.Translucent.NoTitleBar">

        <intent-filter>
            <data android:scheme="YOUR_REDIRECT_SCHEME" android:host="YOUR_REDIRECT_HOST"/>
        </intent-filter>
    </activity>
</application>
```

#### 3. Begin Spotify authorization flow with Activity.startSpotifyImplicitLoginActivity
Now, you just need to begin the authorization flow by calling `Activity.startSpotifyImplicitLoginActivity(spotifyLoginImplementationClass: Class<T>)` in any
activity in your application.

Example:
```kotlin
SpotifyDefaultCredentialStore.activityBackOnImplicitAuth = classBackTo // use if you're using guardValidSpotifyImplicitApi, though this is not recommended
startSpotifyImplicitLoginActivity(SpotifyImplicitLoginActivityImpl::class.java)
```

#### 4. ???

#### 5. Profit (more accurately, your onSuccess or onFailure methods will be called)


### Using SpotifyApi in your application
Based on the type of authorization you used to authenticate the user, you will either call `SpotifyDefaultCredentialStore.getSpotifyImplicitGrantApi`
or `SpotifyDefaultCredentialStore.getSpotifyClientPkceApi`. Both methods allow you to optionally pass parameters to configure the returned
`SpotifyApi`.

You will want to write a guard to handle what happens when `SpotifyException.ReAuthenticationNeededException` is thrown. 

A basic guard is `Activity.guardValidImplicitSpotifyApi`, which will launch the provided 
activity after a user authenticates successfully, if the implicit token has expired.

A more complex guard can be found in the [sample application](https://github.com/Nielssg/Spotify-Api-Test-App/blob/main/app/src/main/java/com/adamratzman/spotifyandroidexample/auth/VerifyLoggedInUtils.kt).

## Spotify Remote integration
Spotify remote integration is still a WIP.

## Broadcast Notifications
You can easily add support for handling Spotify app broadcast notifications by implementing 
the `AbstractSpotifyBroadcastReceiver` class and registering the receiver in a Fragment or 
Activity. 

Supported broadcast types: queue changes, playback state changes, and metadata changes.

Note that "Device Broadcast Status" must be enabled in the Spotify app and the active Spotify device must be the Android 
device that your app is on to receive notifications.

This library provides a `registerSpotifyBroadcastReceiver` method that you can use to 
easily register your created broadcast receiver.

An example implementation of `AbstractSpotifyBroadcastReceiver` and use of `registerSpotifyBroadcastReceiver` 
are provided below. Please see the sample app for a complete implementation.

```kotlin
class SpotifyBroadcastReceiver(val activity: ViewBroadcastsActivity) : AbstractSpotifyBroadcastReceiver() {
    override fun onMetadataChanged(data: SpotifyMetadataChangedData) {
        activity.broadcasts += data
        println("broadcast: ${data}")
    }

    override fun onPlaybackStateChanged(data: SpotifyPlaybackStateChangedData) {
        activity.broadcasts += data
        println("broadcast: $data")
    }

    override fun onQueueChanged(data: SpotifyQueueChangedData) {
        activity.broadcasts += data
        println("broadcast: $data")
    }
}

class ViewBroadcastsActivity : BaseActivity() {
    lateinit var spotifyBroadcastReceiver: SpotifyBroadcastReceiver
    val broadcasts: MutableList<SpotifyBroadcastEventData> = mutableStateListOf()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        spotifyBroadcastReceiver = SpotifyBroadcastReceiver(this)

        ...

        registerSpotifyBroadcastReceiver(spotifyBroadcastReceiver, *SpotifyBroadcastType.values())
    }
}

```

## Compatibility below Android API26

Older versions of Android do not include some of the required Java8 APIs. To target these older APIs, you must enable [Java8 API Desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) in your app.


================================================
FILE: TESTING.md
================================================
# Testing

We use the multiplatform kotlin.test framework to run tests.

You must create a Spotify application [here](https://developer.spotify.com/dashboard/applications) to get credentials.

To run **only** public endpoint tests, you only need `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` as environment variables.

To additionally run **all** private (client) endpoint tests, you need a valid Spotify application, redirect uri, and token string. 
The additional environment variables you will need to add are `SPOTIFY_REDIRECT_URI` and `SPOTIFY_TOKEN_STRING`.

To specifically run player tests, you must include the `SPOTIFY_ENABLE_PLAYER_TESTS`=true environment variable.

Some tests may fail if you do not allow access to all required scopes. To mitigate this, you can individually grant
each scope or use the following code snippet to print out the Spotify token string (given a generated authorization code). 
However, you can painlessly generate a valid token by using this site: https://adamratzman.com/projects/spotify/generate-token

To run tests, run `gradle jvmTest`, `gradle macosX64Test`, `gradle testDebugUnitTest`, or any other target.

To output all http requests to the console, set the `SPOTIFY_LOG_HTTP`=true environment variable.

To build the maven artifact locally, you will need to follow these steps:
- Create `gradle.properties` if it doesn't exist already.
- Follow [this guide](https://gist.github.com/phit/bd3c6d156a2fa5f3b1bc15fa94b3256c). Instead of `.gpg` extension, use `.kbx` for your secring.
- Run `gradle publishToMavenLocal`

You can use this artifact to test locally by adding the `mavenLocal()` repository in any local gradle project.

To build docs, run `gradle dokka`. They will be located under the docs directory in the repostiory root, and 
are ignored. This is how we generate release docs.

================================================
FILE: build.gradle.kts
================================================
@file:Suppress("UnstableApiUsage")

import com.fasterxml.jackson.databind.json.JsonMapper
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput.Target

plugins {
    kotlin("multiplatform")
    `maven-publish`
    signing
    id("com.android.library")
    kotlin("plugin.serialization")
    id("com.diffplug.spotless") version "6.21.0"
    id("com.moowork.node") version "1.3.1"
    id("org.jetbrains.dokka") version "1.9.0"
}

repositories {
    google()
    mavenCentral()
}

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:") // resolved in settings.gradle.kts
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:") // resolved in settings.gradle.kts
    }
}

// --- spotify-web-api-kotlin info ---
val libraryVersion: String = System.getenv("SPOTIFY_API_PUBLISH_VERSION") ?: "0.0.0.SNAPSHOT"

// Publishing credentials (environment variable)
val nexusUsername: String? = System.getenv("NEXUS_USERNAME")
val nexusPassword: String? = System.getenv("NEXUS_PASSWORD")

group = "com.adamratzman"
version = libraryVersion


android {
    namespace = "com.adamratzman.spotify"
    compileSdk = 31
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    packaging {
        resources.excludes.add("META-INF/*.md") // needed to prevent android compilation errors
    }
    defaultConfig {
        minSdk = 23
        setCompileSdkVersion(31)
        testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
    testOptions {
        this.unitTests.isReturnDefaultValues = true
    }
    sourceSets["main"].setRoot("src/androidMain")
    sourceSets["test"].setRoot("src/androidUnitTest")
}

// invoked in kotlin closure, needs to be registered before
val dokkaJar: TaskProvider<Jar> by tasks.registering(Jar::class) {
    group = JavaBasePlugin.DOCUMENTATION_GROUP
    description = "spotify-web-api-kotlin generated documentation"
    from(tasks.dokkaHtml)
    archiveClassifier.set("javadoc")
}

kotlin {
    @OptIn(ExperimentalKotlinGradlePluginApi::class)
    compilerOptions { 
        freeCompilerArgs.add("-Xexpect-actual-classes")
    }
    explicitApiWarning()
    jvmToolchain(17)

    androidTarget {
        compilations.all { kotlinOptions.jvmTarget = "17" }

        mavenPublication { setupPom(artifactId) }

        publishLibraryVariants("debug", "release")

        publishLibraryVariantsGroupedByFlavor = true
    }

    jvm {
        compilations.all {
            kotlinOptions.jvmTarget = "1.8"
        }
        testRuns["test"].executionTask.configure {
            useJUnit()
        }

        mavenPublication { setupPom(artifactId) }
    }

    js(KotlinJsCompilerType.IR) {
        mavenPublication { setupPom(artifactId) }

        browser {
            webpackTask {
                output.globalObject = "this"
                output.libraryTarget = Target.UMD
            }

            testTask {
                useKarma {
                    useChromeHeadless()
                    webpackConfig.cssSupport { isEnabled = true }
                }
            }
        }

        binaries.executable()
    }

    macosX64 {
        mavenPublication { setupPom(artifactId) }
    }

    linuxX64 {
        mavenPublication { setupPom(artifactId) }
    }

    mingwX64 {
        mavenPublication { setupPom(artifactId) }
    }

    iosX64 {
        binaries { framework { baseName = "spotify" } }
        mavenPublication { setupPom(artifactId) }
    }

    iosArm64 {
        binaries { framework { baseName = "spotify" } }
        mavenPublication { setupPom(artifactId) }
    }

    iosSimulatorArm64 {
        binaries { framework { baseName = "spotify" } }
        mavenPublication { setupPom(artifactId) }
    }

    // !! unable to include currently due to korlibs not being available !!
    /*
    tvos {
        binaries { framework { baseName = "spotify" } }

        mavenPublication { setupPom(artifactId) }
    }

    watchos {
        binaries { framework { baseName = "spotify" } }

        mavenPublication { setupPom(artifactId) }
    }*/

    applyDefaultHierarchyTemplate()

    sourceSets {
        val kotlinxDatetimeVersion: String by project
        val kotlinxSerializationVersion: String by project
        val kotlinxCoroutinesVersion: String by project
        val ktorVersion: String by project

        val sparkVersion: String by project
        val korlibsVersion: String by project

        commonMain {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")
                implementation("io.ktor:ktor-client-core:$ktorVersion")
                implementation("com.soywiz.korlibs.krypto:krypto:$korlibsVersion")
                implementation("com.soywiz.korlibs.korim:korim:$korlibsVersion")
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDatetimeVersion")
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
            }
        }

        commonTest {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion")
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
            }
        }

        val commonJvmLikeMain by creating {
            dependsOn(commonMain.get())

            dependencies {
                implementation("net.sourceforge.streamsupport:android-retrofuture:1.7.3")
            }
        }

        val commonJvmLikeTest by creating {
            dependencies {
                implementation(kotlin("test-junit"))
                implementation("com.sparkjava:spark-core:$sparkVersion")
                runtimeOnly(kotlin("reflect"))
            }
        }

        val commonNonJvmTargetsTest by creating {
            dependsOn(commonTest.get())
        }

        jvmMain {
            dependsOn(commonJvmLikeMain)

            repositories {
                mavenCentral()
            }

            dependencies {
                implementation("io.ktor:ktor-client-cio:$ktorVersion")
            }
        }

        jvmTest.get().dependsOn(commonJvmLikeTest)

        jsMain {
            dependencies {
                implementation("io.ktor:ktor-client-js:$ktorVersion")
                implementation(kotlin("stdlib-js"))
            }
        }

        jsTest {
            dependsOn(commonNonJvmTargetsTest)

            dependencies {
                implementation(kotlin("test-js"))
            }
        }

        androidMain {
            dependsOn(commonJvmLikeMain)

            repositories {
                mavenCentral()
            }

            dependencies {
                val androidSpotifyAuthVersion: String by project
                val androidCryptoVersion: String by project
                val androidxCompatVersion: String by project

                api("com.spotify.android:auth:$androidSpotifyAuthVersion")
                implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
                implementation("androidx.security:security-crypto:$androidCryptoVersion")
                implementation("androidx.appcompat:appcompat:$androidxCompatVersion")
            }
        }

        val androidUnitTest by getting {
            dependsOn(commonJvmLikeTest)
        }

        // desktop targets
        // as kotlin/native, they require special ktor versions
        val desktopMain by creating {
            dependsOn(commonMain.get())

            dependencies {
                implementation("io.ktor:ktor-client-curl:$ktorVersion")
            }
        }

        linuxMain.get().dependsOn(desktopMain)
        mingwMain.get().dependsOn(desktopMain)
        macosMain.get().dependsOn(desktopMain)

        val desktopTest by creating { dependsOn(commonNonJvmTargetsTest) }
        linuxTest.get().dependsOn(desktopTest)
        mingwTest.get().dependsOn(desktopTest)
        macosTest.get().dependsOn(desktopTest)

        // darwin targets

        val nativeDarwinMain by creating {
            dependsOn(commonMain.get())

            dependencies {
                implementation("io.ktor:ktor-client-ios:$ktorVersion")
            }
        }

        val nativeDarwinTest by creating { dependsOn(commonNonJvmTargetsTest) }

        iosMain.get().dependsOn(nativeDarwinMain)
        iosTest.get().dependsOn(nativeDarwinTest)

        all {
            languageSettings.optIn("kotlin.RequiresOptIn")
            languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
        }
    }

    publishing {
        registerPublishing()
    }
}

tasks {
    dokkaHtml {
        outputDirectory.set(projectDir.resolve("docs"))

        dokkaSourceSets {
            configureEach {
                skipDeprecated.set(true)

                sourceLink {
                    localDirectory.set(file("src"))
                    remoteUrl.set(uri("https://github.com/adamint/spotify-web-api-kotlin/tree/master/src").toURL())
                    remoteLineSuffix.set("#L")
                }
            }
        }
    }

    spotless {
        kotlin {
            target("**/*.kt")
            licenseHeader("/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2023; Original author: Adam Ratzman */")
            ktlint()
        }
    }


    val publishAllPublicationsToNexusRepositoryWithTests by registering(Task::class) {
        dependsOn.add(check)
        dependsOn.add("publishAllPublicationsToNexusRepository")
        dependsOn.add(dokkaHtml)
    }

    withType<Test> {
        testLogging {
            showStandardStreams = true
        }
    }

    val packForXcode by creating(Sync::class) {
        group = "build"
        val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
        val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator"
        val targetName = "ios" + if (sdkName.startsWith("iphoneos")) "Arm64" else "X64"
        val framework = kotlin.targets.getByName<KotlinNativeTarget>(targetName).binaries.getFramework(mode)
        inputs.property("mode", mode)
        dependsOn(framework.linkTask)
        val targetDir = File(layout.buildDirectory.asFile.get(), "xcode-frameworks")
        from({ framework.outputDirectory })
        into(targetDir)
    }
    getByName("build").dependsOn(packForXcode)
}

val signingTasks = tasks.withType<Sign>()
tasks.withType<AbstractPublishToMaven>().configureEach {
    dependsOn(signingTasks)
}


fun MavenPublication.setupPom(publicationName: String) {
    artifactId = artifactId.replace("-web", "")
    artifact(dokkaJar.get()) // add javadocs to publication

    pom {
        name.set(publicationName)
        description.set("A Kotlin wrapper for the Spotify Web API.")
        url.set("https://github.com/adamint/spotify-web-api-kotlin")
        inceptionYear.set("2018")

        scm {
            url.set("https://github.com/adamint/spotify-web-api-kotlin")
            connection.set("scm:https://github.com/adamint/spotify-web-api-kotlin.git")
            developerConnection.set("scm:git://github.com/adamint/spotify-web-api-kotlin.git")
        }

        licenses {
            license {
                name.set("MIT License")
                url.set("https://github.com/adamint/spotify-web-api-kotlin/blob/master/LICENSE")
                distribution.set("repo")
            }
        }
        developers {
            developer {
                id.set("adamratzman")
                name.set("Adam Ratzman")
                email.set("adam@adamratzman.com")
            }
        }
    }
}


// --- Publishing ---

fun PublishingExtension.registerPublishing() {
    publications {
        val kotlinMultiplatform by getting(MavenPublication::class) {
            artifactId = "spotify-api-kotlin-core"
            setupPom(artifactId)
        }
    }

    repositories {
        maven {
            name = "nexus"

            // Publishing locations
            val releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
            val snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/"

            url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl)

            credentials {
                username = nexusUsername
                password = nexusPassword
            }
        }
    }
}

// --- Signing ---
val signingKey = project.findProperty("SIGNING_KEY") as? String
val signingPassword = project.findProperty("SIGNING_PASSWORD") as? String

signing {
    if (signingKey != null && signingPassword != null) {
        useInMemoryPgpKeys(
            project.findProperty("SIGNING_KEY") as? String,
            project.findProperty("SIGNING_PASSWORD") as? String
        )
        sign(publishing.publications)
    }
}

// Test tasks
tasks.register("updateNonJvmTestFakes") {
    if (System.getenv("SPOTIFY_TOKEN_STRING") == null
        || System.getenv("SHOULD_RECACHE_RESPONSES")?.toBoolean() != true
    ) {
        return@register
    }

    dependsOn("jvmTest")
    val responseCacheDir =
        System.getenv("RESPONSE_CACHE_DIR")?.let { File(it) }
            ?: throw IllegalArgumentException("No response cache directory provided")
    val commonTestResourcesSource = projectDir.resolve("src/commonTest/resources")
    if (!commonTestResourcesSource.exists()) commonTestResourcesSource.mkdir()

    val commonTestResourceFileToSet = commonTestResourcesSource.resolve("cached_responses.json")

    if (commonTestResourceFileToSet.exists()) commonTestResourceFileToSet.delete()
    commonTestResourceFileToSet.createNewFile()

    val testToOrderedResponseMap: Map<String, List<String>> = responseCacheDir.walk()
        .filter { it.isFile && it.name.matches("http_request_\\d+.txt".toRegex()) }
        .groupBy { "${it.parentFile.parentFile.name}.${it.parentFile.name}" }
        .map { (key, group) -> key to group.sorted().map { it.readText() } }
        .toMap()

    val jsonLiteral = JsonMapper().writeValueAsString(testToOrderedResponseMap)
    commonTestResourceFileToSet.writeText(jsonLiteral)
    println(commonTestResourceFileToSet.absolutePath)
}

================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists


================================================
FILE: gradle.properties
================================================
systemProp.org.gradle.internal.publish.checksums.insecure=true

org.gradle.daemon=true
org.gradle.jvmargs=-Xmx8000m
org.gradle.caching=true

# android target settings
android.useAndroidX=true
android.enableJetifier=true

# language dependencies
kotlinVersion=1.9.22

# library dependencies
kotlinxDatetimeVersion=0.5.0
kotlinxSerializationVersion=1.6.2
ktorVersion=2.3.8
korlibsVersion=3.4.0
kotlinxCoroutinesVersion=1.7.3

androidBuildToolsVersion=8.2.2
androidSpotifyAuthVersion=2.1.1
androidCryptoVersion=1.1.0-alpha06
androidxCompatVersion=1.7.0-alpha03

sparkVersion=2.9.4

================================================
FILE: gradlew
================================================
#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The "traditional" practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in "$@", and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
    [ -h "$app_path" ]
do
    ls=$( ls -ld "$app_path" )
    link=${ls#*' -> '}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo "$*"
} >&2

die () {
    echo
    echo "$*"
    echo
    exit 1
} >&2

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD=java
    if ! command -v java >/dev/null 2>&1
    then
        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
    case $MAX_FD in #(
      max*)
        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        MAX_FD=$( ulimit -H -n ) ||
            warn "Could not query maximum file descriptor limit"
    esac
    case $MAX_FD in  #(
      '' | soft) :;; #(
      *)
        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        ulimit -n "$MAX_FD" ||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )

    JAVACMD=$( cygpath --unix "$JAVACMD" )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don't mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e "$t" ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed "$arg" )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- "$@" "$arg"      # push replacement arg
    done
fi


# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command:
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
#     and any embedded shellness will be escaped.
#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
#     treated as '${Hostname}' itself on the command line.

set -- \
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
        -classpath "$CLASSPATH" \
        org.gradle.wrapper.GradleWrapperMain \
        "$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
    die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
        xargs -n1 |
        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
        tr '\n' ' '
    )" '"$@"'

exec "$JAVACMD" "$@"


================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: publish_all.sh
================================================
gradle publishMacosX64PublicationToNexusRepository publishLinuxX64PublicationToNexusRepository publishKotlinMultiplatformPublicationToNexusRepository publishTvosX64PublicationToNexusRepository publishTvosArm64PublicationToNexusRepository publishIosX64PublicationToNexusRepository publishIosArm64PublicationToNexusRepository publishJsPublicationToNexusRepository publishJvmPublicationToNexusRepository publishAndroidPublicationToNexusRepository

================================================
FILE: settings.gradle.kts
================================================
pluginManagement {
    val kotlinVersion: String by settings
    val androidBuildToolsVersion: String by settings

    plugins {
        id("org.jetbrains.kotlin.multiplatform").version(kotlinVersion)
        id("org.jetbrains.kotlin.plugin.serialization").version(kotlinVersion)
        id("org.jetbrains.dokka").version(kotlinVersion)
    }

    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == "kotlin-multiplatform") {
                useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
            }
            if (requested.id.id == "org.jetbrains.kotlin.jvm") {
                useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
            }
            if (requested.id.id == "kotlinx-serialization") {
                useModule("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion")
            } else if (requested.id.id == "com.android.library") {
                useModule("com.android.tools.build:gradle:$androidBuildToolsVersion")
            } else if (requested.id.id == "kotlin-android-extensions") {
                useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
            }
        }
    }

    repositories {
        mavenCentral()
        gradlePluginPortal()
        google()
        maven { url = java.net.URI("https://plugins.gradle.org/m2/") }
    }
}

rootProject.name = "spotify-api-kotlin"

================================================
FILE: src/androidMain/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.adamratzman.spotify">

</manifest>

================================================
FILE: src/androidMain/kotlin/com/adamratzman/spotify/auth/SpotifyDefaultCredentialStore.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.auth

import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.adamratzman.spotify.GenericSpotifyApi
import com.adamratzman.spotify.SpotifyApi
import com.adamratzman.spotify.SpotifyApiOptions
import com.adamratzman.spotify.SpotifyClientApi
import com.adamratzman.spotify.SpotifyImplicitGrantApi
import com.adamratzman.spotify.SpotifyUserAuthorization
import com.adamratzman.spotify.models.Token
import com.adamratzman.spotify.refreshSpotifyClientToken
import com.adamratzman.spotify.spotifyClientPkceApi
import com.adamratzman.spotify.spotifyImplicitGrantApi
import com.adamratzman.spotify.utils.logToConsole

/**
 * Provided credential store for holding current Spotify token credentials, allowing you to easily store and retrieve
 * Spotify tokens. Recommended in most use-cases.
 *
 * @param clientId The client id associated with your application
 * @param applicationContext The application context - you can obtain this by storing your application context statically (such as with a companion object)
 *
 */
@RequiresApi(Build.VERSION_CODES.M)
public class SpotifyDefaultCredentialStore(
    private val clientId: String,
    private val redirectUri: String,
    applicationContext: Context
) {
    public companion object {
        /**
         * The key used with spotify scope string in [EncryptedSharedPreferences]
         */
        public const val SpotifyScopeStringKey: String = "spotifyTokenScopes"

        /**
         * The key used with spotify token expiry in [EncryptedSharedPreferences]
         */
        public const val SpotifyTokenExpiryKey: String = "spotifyTokenExpiry"

        /**
         * The key used with spotify access token in [EncryptedSharedPreferences]
         */
        public const val SpotifyAccessTokenKey: String = "spotifyAccessToken"

        /**
         * The key used with spotify refresh token in [EncryptedSharedPreferences]
         */
        public const val SpotifyRefreshTokenKey: String = "spotifyRefreshToken"

        /**
         * The PKCE code verifier key currently being used in [EncryptedSharedPreferences]
         */
        public const val SpotifyCurrentPkceCodeVerifierKey: String =
            "spotifyCurrentPkceCodeVerifier"

        /**
         * The activity to return to if re-authentication is necessary on implicit authentication. Null except during authentication when using [guardValidImplicitSpotifyApi]
         */
        public var activityBackOnImplicitAuth: Class<out Activity>? = null
    }

    public var credentialTypeStored: CredentialType? = null

    private val masterKeyForEncryption =
        MasterKey.Builder(applicationContext).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()

    /**
     * The [EncryptedSharedPreferences] that this API saves to/retrieves from.
     */
    public val encryptedPreferences: SharedPreferences = EncryptedSharedPreferences.create(
        applicationContext,
        "spotify-api-encrypted-preferences",
        masterKeyForEncryption,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )


    /**
     * Get/set when the Spotify access token will expire, in milliseconds from UNIX epoch. This will be one hour from authentication.
     */
    public var spotifyTokenExpiresAt: Long?
        get() {
            val expiry = encryptedPreferences.getLong(SpotifyTokenExpiryKey, -1)
            return if (expiry == -1L) null else expiry
        }
        set(value) {
            if (value == null) {
                encryptedPreferences.edit().remove(SpotifyTokenExpiryKey).apply()
            } else {
                encryptedPreferences.edit().putLong(SpotifyTokenExpiryKey, value).apply()
            }
        }

    /**
     * Get/set the Spotify access token (the access token string, not the wrapped [Token]).
     */
    public var spotifyAccessToken: String?
        get() = encryptedPreferences.getString(SpotifyAccessTokenKey, null)
        set(value) = encryptedPreferences.edit().putString(SpotifyAccessTokenKey, value).apply()

    /**
     * Get/set the Spotify refresh token.
     */
    public var spotifyRefreshToken: String?
        get() = encryptedPreferences.getString(SpotifyRefreshTokenKey, null)
        set(value) = encryptedPreferences.edit().putString(SpotifyRefreshTokenKey, value).apply()

    /**
     * Get/set the Spotify scope string.
     */
    public var spotifyScopeString: String?
        get() = encryptedPreferences.getString(SpotifyScopeStringKey, null)
        set(value) = encryptedPreferences.edit().putString(SpotifyScopeStringKey, value).apply()

    /**
     * Get/set the current Spotify PKCE code verifier.
     */
    public var currentSpotifyPkceCodeVerifier: String?
        get() = encryptedPreferences.getString(SpotifyCurrentPkceCodeVerifierKey, null)
        set(value) = encryptedPreferences.edit().putString(SpotifyCurrentPkceCodeVerifierKey, value)
            .apply()

    /**
     * Get/set the Spotify [Token] obtained from [spotifyToken].
     * If the token has expired according to [spotifyTokenExpiresAt], this will return null.
     */
    public var spotifyToken: Token?
        get() {
            val tokenExpiresAt = spotifyTokenExpiresAt ?: return null
            val accessToken = spotifyAccessToken ?: return null
            if (tokenExpiresAt < System.currentTimeMillis()) return null

            val refreshToken = spotifyRefreshToken
            return Token(
                accessToken,
                "Bearer",
                (tokenExpiresAt - System.currentTimeMillis()).toInt() / 1000,
                refreshToken,
                scopeString = spotifyScopeString
            )
        }
        set(token) {
            if (token == null) {
                spotifyAccessToken = null
                spotifyTokenExpiresAt = null
                spotifyRefreshToken = null

                credentialTypeStored = null
                spotifyScopeString = null
            } else {
                spotifyAccessToken = token.accessToken
                spotifyTokenExpiresAt = token.expiresAt
                spotifyRefreshToken = token.refreshToken
                spotifyScopeString = token.scopeString

                credentialTypeStored =
                    if (token.refreshToken != null) CredentialType.Pkce else CredentialType.ImplicitGrant
            }
        }

    /**
     * Create a new [SpotifyImplicitGrantApi] instance using the [spotifyToken] stored using this credential store.
     *
     * @param block Applied configuration to the [SpotifyImplicitGrantApi]
     */
    public fun getSpotifyImplicitGrantApi(block: ((SpotifyApiOptions).() -> Unit)? = null): SpotifyImplicitGrantApi? {
        val token = spotifyToken ?: return null
        return spotifyImplicitGrantApi(clientId, token, block ?: {})
    }

    /**
     * Create a new [SpotifyClientApi] instance using the [spotifyToken] stored using this credential store.
     *
     * @param block Applied configuration to the [SpotifyClientApi]
     */
    public suspend fun getSpotifyClientPkceApi(block: ((SpotifyApiOptions).() -> Unit)? = null): SpotifyClientApi? {
        val token = spotifyToken
            ?: if (spotifyRefreshToken != null) {
                val newToken = refreshSpotifyClientToken(clientId, null, spotifyRefreshToken, true)
                spotifyToken = newToken
                newToken
            } else {
                return null
            }

        return spotifyClientPkceApi(
            clientId,
            redirectUri,
            SpotifyUserAuthorization(token = token),
            block ?: {}
        ).build().apply {
            val previousAfterTokenRefresh = spotifyApiOptions.afterTokenRefresh
            spotifyApiOptions.afterTokenRefresh = {
                spotifyToken = this.token
                logToConsole("Refreshed Spotify PKCE token in credential store... $token")
                previousAfterTokenRefresh?.invoke(this)
            }
        }
    }

    /**
     * Sets [spotifyToken] using [SpotifyApi.token]. This wraps around [spotifyToken]'s setter.
     *
     * @param api A valid [GenericSpotifyApi]
     */
    public fun setSpotifyApi(api: GenericSpotifyApi) {
        spotifyToken = api.token
    }

    /**
     * Returns whether the [Token] stored in this Credential Store is refreshable (whether there is a refresh token associated
     * with it).
     */
    public fun canApiBeRefreshed(): Boolean {
        return spotifyRefreshToken != null
    }

    /**
     * Clear the [SharedPreferences] instance corresponding to the Spotify credentials.
     */
    @SuppressLint("ApplySharedPref")
    public fun clear(): Boolean = try {
        encryptedPreferences.edit().clear().commit()
    } catch (e: Exception) {
        // This might crash, encrypted preferences is still alpha...
        false
    }
}

public enum class CredentialType {
    ImplicitGrant,
    Pkce
}

@RequiresApi(Build.VERSION_CODES.M)
public fun Application.getDefaultCredentialStore(
    clientId: String,
    redirectUri: String
): SpotifyDefaultCredentialStore {
    return SpotifyDefaultCredentialStore(clientId, redirectUri, applicationContext)
}


================================================
FILE: src/androidMain/kotlin/com/adamratzman/spotify/auth/implicit/AbstractSpotifyAppCompatImplicitLoginActivity.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.auth.implicit

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

/**
 * Wrapper around spotify-auth's [LoginActivity] that allows configuration of the authentication process, along with
 * callbacks on successful and failed authentication. Pair this with [SpotifyDefaultCredentialStore] to easily store credentials.
 * Inherits from [AppCompatActivity]. If instead you want to inherit from [Activity], please use [AbstractSpotifyAppImplicitLoginActivity].
 *
 */
public abstract class AbstractSpotifyAppCompatImplicitLoginActivity : SpotifyImplicitLoginActivity,
    AppCompatActivity() {
    @Suppress("LeakingThis")
    public override val activity: Activity = this
    public override val useDefaultRedirectHandler: Boolean = true

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        triggerLoginActivity()
    }

    @Suppress("OVERRIDE_DEPRECATION")
    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
        super.onActivityResult(requestCode, resultCode, intent)
        processActivityResult(requestCode, resultCode, intent)
    }
}


================================================
FILE: src/androidMain/kotlin/com/adamratzman/spotify/auth/implicit/AbstractSpotifyAppImplicitLoginActivity.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.auth.implicit

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.spotify.sdk.android.auth.LoginActivity

/**
 * Wrapper around spotify-auth's [LoginActivity] that allows configuration of the authentication process, along with
 * callbacks on successful and failed authentication. Pair this with [SpotifyDefaultCredentialStore] to easily store credentials.
 * Inherits from [Activity]. If instead you want to inherit from [AppCompatActivity], please use [AbstractSpotifyAppCompatImplicitLoginActivity].
 *
 */
public abstract class AbstractSpotifyAppImplicitLoginActivity : SpotifyImplicitLoginActivity, Activity() {
    @Suppress("LeakingThis")
    public override val activity: Activity = this
    public override val useDefaultRedirectHandler: Boolean = true

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        triggerLoginActivity()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
        super.onActivityResult(requestCode, resultCode, intent)
        processActivityResult(requestCode, resultCode, intent)
    }
}


================================================
FILE: src/androidMain/kotlin/com/adamratzman/spotify/auth/implicit/ImplicitAuthUtils.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.auth.implicit

import android.app.Activity
import android.content.Intent
import com.adamratzman.spotify.SpotifyException
import com.adamratzman.spotify.auth.SpotifyDefaultCredentialStore.Companion.activityBackOnImplicitAuth

// Starting implicit login activity

/**
 * Start Spotify implicit login activity within an existing activity.
 */
public inline fun <reified T : SpotifyImplicitLoginActivity> Activity.startSpotifyImplicitLoginActivity() {
    startSpotifyImplicitLoginActivity(T::class.java)
}

/**
 * Start Spotify implicit login activity within an existing activity.
 *
 * @param spotifyLoginImplementationClass Your implementation of [SpotifyImplicitLoginActivity], defining what to do on Spotify login
 */
public fun <T : SpotifyImplicitLoginActivity> Activity.startSpotifyImplicitLoginActivity(spotifyLoginImplementationClass: Class<T>) {
    startActivity(Intent(this, spotifyLoginImplementationClass))
}

/**
 * Basic implicit authentication guard - verifies that the user is logged in to Spotify and uses [SpotifyDefaultImplicitAuthHelper] to
 * handle re-authentication and redirection back to the activity.
 *
 * Note: this should only be used for small applications.
 *
 * @param spotifyImplicitLoginImplementationClass Your implementation of [SpotifyImplicitLoginActivity], defining what to do on Spotify login
 * @param classBackTo The activity to return to if re-authentication is necessary
 * @block The code block to execute
 */
public fun <T> Activity.guardValidImplicitSpotifyApi(
    spotifyImplicitLoginImplementationClass: Class<out SpotifyImplicitLoginActivity>,
    classBackTo: Class<out Activity>? = null,
    block: () -> T
): T? {
    return try {
        block()
    } catch (e: SpotifyException.ReAuthenticationNeededException) {
        activityBackOnImplicitAuth = classBackTo
        startSpotifyImplicitLoginActivity(spotifyImplicitLoginImplementationClass)
        null
    }
}


================================================
FILE: src/androidMain/kotlin/com/adamratzman/spotify/auth/implicit/SpotifyImplicitLoginActivity.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.auth.implicit

import android.app.Activity
import android.content.Intent
import com.adamratzman.spotify.SpotifyImplicitGrantApi
import com.adamratzman.spotify.SpotifyScope
import com.adamratzman.spotify.auth.SpotifyDefaultCredentialStore
import com.adamratzman.spotify.auth.SpotifyDefaultCredentialStore.Companion.activityBackOnImplicitAuth
import com.adamratzman.spotify.models.Token
import com.adamratzman.spotify.spotifyImplicitGrantApi
import com.adamratzman.spotify.utils.logToConsole
import com.spotify.sdk.android.auth.AuthorizationClient
import com.spotify.sdk.android.auth.AuthorizationRequest
import com.spotify.sdk.android.auth.AuthorizationResponse
import com.spotify.sdk.android.auth.LoginActivity

/**
 * Wrapper around spotify-auth's [LoginActivity] that allows configuration of the authentication process, along with
 * callbacks on successful and failed authentication. Pair this with [SpotifyDefaultCredentialStore] to easily store credentials.
 * To use, you must extend from either [AbstractSpotifyAppImplicitLoginActivity] or [AbstractSpotifyAppCompatImplicitLoginActivity]
 *
 * @property state The state to use to verify the login request.
 * @property clientId Your application's Spotify client id.
 * @property clientId Your application's Spotify client secret.
 * @property redirectUri Your application's Spotify redirect id - NOTE that this should be an android scheme (such as spotifyapp://authback)
 * and that this must be registered in your manifest.
 * @property useDefaultRedirectHandler Disable if you will not be using [useDefaultRedirectHandler] but will be setting [SpotifyDefaultImplicitAuthHelper.activityBackOnImplicitAuth].
 */
public interface SpotifyImplicitLoginActivity {
    public val activity: Activity

    public val state: Int
    public val clientId: String
    public val redirectUri: String
    public val useDefaultRedirectHandler: Boolean

    /**
     * Return the scopes that you are going to request from the user here.
     */
    public fun getRequestingScopes(): List<SpotifyScope>

    /**
     * Override this to define what to do after authentication has been successfully completed. A valid, usable
     * [spotifyApi] is provided to you. You may likely want to use [SpotifyDefaultCredentialStore] to store/retrieve this token.
     *
     * @param spotifyApi Valid, usable [SpotifyImplicitGrantApi] that you can use to make requests.
     */
    public fun onSuccess(spotifyApi: SpotifyImplicitGrantApi)

    /**
     * Override this to define what to do after authentication has failed. You may want to use [SpotifyDefaultCredentialStore] to remove any stored token.
     */
    public fun onFailure(errorMessage: String)

    /**
     * Override this to define what to do after [onSuccess] has run.
     * The default behavior is to finish the activity, and redirect the user back to the activity set on [SpotifyDefaultCredentialStore.activityBackOnImplicitAuth]
     * only if [guardValidImplicitSpotifyApi] has been used or if [SpotifyDefaultCredentialStore.activityBackOnImplicitAuth] has been set.
     */
    public fun redirectAfterOnSuccessAuthentication() {
        if (useDefaultRedirectHandler && activityBackOnImplicitAuth != null) {
            activity.startActivity(Intent(activity, activityBackOnImplicitAuth))
            activityBackOnImplicitAuth = null
        }
        activity.finish()
    }

    /**
     * Trigger the actual spotify-auth login activity to authenticate the user.
     */
    public fun triggerLoginActivity() {
        val authorizationRequest = AuthorizationRequest.Builder(clientId, AuthorizationResponse.Type.TOKEN, redirectUri)
            .setScopes(getRequestingScopes().map { it.uri }.toTypedArray())
            .setState(state.toString())
            .build()
        logToConsole("Triggering spotify-auth login for url ${authorizationRequest.toUri().path}")
        AuthorizationClient.openLoginActivity(activity, state, authorizationRequest)
    }

    /**
     * Processes the result of [LoginActivity], invokes callbacks, then finishes.
     */
    public fun processActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
        if (requestCode == state) {
            val response = AuthorizationClient.getResponse(resultCode, intent)
            logToConsole("Got implicit auth response of ${response.type}")
            when {
                response.type == AuthorizationResponse.Type.TOKEN -> {
                    val token = Token(
                        response.accessToken,
                        response.type.name,
                        response.expiresIn
                    )
                    val api = spotifyImplicitGrantApi(
                        clientId = clientId,
                        token = token
                    )
                    logToConsole("Built implicit grant api. Executing success handler..")
                    onSuccess(api)
                    redirectAfterOnSuccessAuthentication()
                }
                // AuthorizationResponse.Type.CODE -> TODO()
                // AuthorizationResponse.Type.UNKNOWN -> TODO()
                response.type == AuthorizationResponse.Type.ERROR -> {
                    logToConsole("Got error in authorization... executing error handler")
                    onFailure(response.error ?: "Generic authentication error")
                }
                response.type == AuthorizationResponse.Type.EMPTY -> {
                    logToConsole("Got empty authorization... executing error handler")
                    onFailure(response.error ?: "Authentication empty")
                }
            }
            activity.finish()
        }
    }
}


================================================
FILE: src/androidMain/kotlin/com/adamratzman/spotify/auth/pkce/AbstractSpotifyPkceLoginActivity.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.auth.pkce

import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import com.adamratzman.spotify.R
import com.adamratzman.spotify.SpotifyApiOptions
import com.adamratzman.spotify.SpotifyClientApi
import com.adamratzman.spotify.SpotifyScope
import com.adamratzman.spotify.SpotifyUserAuthorization
import com.adamratzman.spotify.auth.SpotifyDefaultCredentialStore
import com.adamratzman.spotify.auth.getDefaultCredentialStore
import com.adamratzman.spotify.getSpotifyPkceAuthorizationUrl
import com.adamratzman.spotify.getSpotifyPkceCodeChallenge
import com.adamratzman.spotify.spotifyClientPkceApi
import com.adamratzman.spotify.utils.logToConsole
import com.spotify.sdk.android.auth.AuthorizationResponse
import kotlinx.coroutines.runBlocking
import kotlin.random.Random

/**
 * This class hooks into spotify-web-api-kotlin to provide PKCE authorization for Android application. Paired with [SpotifyDefaultCredentialStore] to easily store credentials.
 * To use, you must extend this class and follow the instructions in the spotify-web-api-kotlin README.
 *
 * @property state The state to use to verify the login request.
 * @property clientId Your application's Spotify client id.
 * @property clientId Your application's Spotify client secret.
 * @property redirectUri Your application's Spotify redirect id - NOTE that this should be an android scheme (such as spotifyapp://authback)
 * and that this must be registered in your manifest.
 * @property scopes the scopes that you are going to request from the user here.
 * @property pkceCodeVerifier The code verifier generated that the client will be authenticated with (using its code challenge).
 * Must be between 43-128 alphanumeric characters
 * @property options Provide if you would like to customize the returned [SpotifyClientApi].
 */
@RequiresApi(Build.VERSION_CODES.M)
public abstract class AbstractSpotifyPkceLoginActivity : AppCompatActivity() {
    public abstract val clientId: String
    public abstract val redirectUri: String
    public abstract val scopes: List<SpotifyScope>
    public open val pkceCodeVerifier: String = (0..96).joinToString("") {
        (('a'..'z') + ('A'..'Z') + ('0'..'9')).random().toString()
    }
    public open val state: String = Random.nextLong().toString()
    public open val options: ((SpotifyApiOptions).() -> Unit)? = null

    /**
     * Custom logic to invoke when loading begins ([isLoading] is true) or ends ([isLoading] is false).
     * You can update the view here.
     */
    public open fun setLoadingContent(isLoading: Boolean): () -> Unit = {}

    private lateinit var authorizationIntent: Intent
    private lateinit var credentialStore: SpotifyDefaultCredentialStore

    /**
     * Get the code challenge for the [pkceCodeVerifier] that will be used to confirm token identity.
     */
    public fun getPkceCodeChallenge(): String = getSpotifyPkceCodeChallenge(pkceCodeVerifier)

    /**
     * Get the authorization url that the client will be redirected to during PKCE authorization.
     */
    public fun getAuthorizationUrl(): Uri = getSpotifyPkceAuthorizationUrl(
        *scopes.toTypedArray(),
        clientId = clientId,
        redirectUri = redirectUri,
        codeChallenge = getPkceCodeChallenge(),
        state = state
    ).let { Uri.parse(it) }

    /**
     * The callback that will be executed after successful PKCE authorization.
     *
     * @param api The built [SpotifyClientApi] corresponding to the retrieved token from PKCE auth.
     */
    public abstract fun onSuccess(api: SpotifyClientApi)

    /**
     * The callback that will be executed after unsuccessful PKCE authorization.
     *
     * @param exception The root cause of the auth failure.
     */
    public abstract fun onFailure(exception: Exception)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.spotify_pkce_auth_layout)

        credentialStore = application.getDefaultCredentialStore(clientId, redirectUri)

        // This activity is recreated on every launch, therefore we need to make sure not to
        // launch the activity when a Spotify intent result has been received
        if (intent?.isSpotifyPkceAuthIntent(redirectUri) == false) {
            authorizationIntent = Intent(Intent.ACTION_VIEW, getAuthorizationUrl())
            credentialStore.currentSpotifyPkceCodeVerifier = pkceCodeVerifier
            startActivity(authorizationIntent)
            finish()
        }
    }

    /**
     * User has accepted Spotify permissions at the website and has been redirected to the app, though the app was not open
     */
    override fun onResume() {
        super.onResume()
        if (intent?.isSpotifyPkceAuthIntent(redirectUri) == true) {
            runBlocking { handleSpotifyAuthenticationResponse(AuthorizationResponse.fromUri(intent?.data)) }
        }
    }

    /**
     * User accepted Spotify permissions at the website and has been redirected to the app
     */
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        if (intent?.data != null) setIntent(intent)
    }

    /**
     * Handle the authentication response, only allowing a "code" as response type
     */
    private suspend fun handleSpotifyAuthenticationResponse(response: AuthorizationResponse) {
        logToConsole("Got pkce auth response of ${response.type}")
        if (response.type != AuthorizationResponse.Type.CODE) {
            if (response.type == AuthorizationResponse.Type.TOKEN ||
                response.type == AuthorizationResponse.Type.ERROR ||
                response.type == AuthorizationResponse.Type.EMPTY ||
                response.type == AuthorizationResponse.Type.UNKNOWN
            ) {
                logToConsole("Got invalid response type... executing error handler")
                onFailure(
                    IllegalStateException("Received response type ${response.type} which is not code.")
                )
            }

            finish()
        } else {
            val authorizationCode = response.code
            if (authorizationCode.isNullOrBlank()) {
                logToConsole("Auth code was null or blank... executing error handler")
                onFailure(
                    IllegalStateException("Authorization code was null or blank.")
                )
            } else {
                try {
                    logToConsole("Building client PKCE api...")
                    setLoadingContent(true)
                    val api = spotifyClientPkceApi(
                        clientId = clientId,
                        redirectUri = redirectUri,
                        authorization = SpotifyUserAuthorization(
                            authorizationCode = authorizationCode,
                            pkceCodeVerifier = credentialStore.currentSpotifyPkceCodeVerifier
                        ),
                        options ?: {}
                    ).build()

                    logToConsole("Successfully built client PKCE api")
                    if (api.token.accessToken.isNotBlank()) {
                        credentialStore.spotifyToken = api.token
                        setLoadingContent(false)
                        logToConsole("Successful PKCE auth. Executing success handler..")
                        onSuccess(api)
                    } else {
                        setLoadingContent(false)
                        logToConsole("Failed PKCE auth - API token was blank. Executing success handler..")
                        onFailure(
                            IllegalArgumentException("API token was blank")
                        )
                    }
                } catch (exception: Exception) {
                    setLoadingContent(false)
                    logToConsole("Got error in authorization... executing error handler")
                    onFailure(exception)
                }
            }

            setLoadingContent(false)
            finish()
        }
    }
}


================================================
FILE: src/androidMain/kotlin/com/adamratzman/spotify/auth/pkce/PkceAuthUtils.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.auth.pkce

import android.app.Activity
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi

public fun Intent?.isSpotifyPkceAuthIntent(redirectUri: String): Boolean {
    return this != null &&
        (dataString?.startsWith("$redirectUri/?code=") == true || dataString?.startsWith("$redirectUri/?error=") == true)
}

/**
 * Start Spotify PKCE login activity within an existing activity.
 *
 * @param spotifyLoginImplementationClass Your implementation of [AbstractSpotifyPkceLoginActivity], defining what to do on Spotify PKCE login
 */
@RequiresApi(Build.VERSION_CODES.M)
public fun Activity.startSpotifyClientPkceLoginActivity(spotifyLoginImplementationClass: Class<out AbstractSpotifyPkceLoginActivity>) {
    val intent = Intent(this, spotifyLoginImplementationClass)
    startActivity(intent)
}


================================================
FILE: src/androidMain/kotlin/com/adamratzman/spotify/notifications/AbstractSpotifyBroadcastReceiver.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.notifications

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.adamratzman.spotify.models.PlayableUri
import com.adamratzman.spotify.notifications.AbstractSpotifyBroadcastReceiver.Companion.BaseSpotifyNotificationId
import com.adamratzman.spotify.utils.logToConsole

/**
 * If you are developing an Android application and want to know what is happening in the Spotify app,
 * you can subscribe to broadcast notifications from it. The Spotify app can posts sticky media broadcast notifications
 * that can be read by any app on the same Android device. The media notifications contain information about what is
 * currently being played in the Spotify App, as well as the playback position and the playback status of the app.
 *
 * Note that media notifications need to be enabled manually in the Spotify app
 *
 * You need to extend this class and register it, whether through the manifest or fragment/activity to receive notifications, as
 * well as overriding [onPlaybackStateChanged], [onQueueChanged], and/or [onMetadataChanged].
 *
 */
public abstract class AbstractSpotifyBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val timeSentInMs = intent.getLongExtra("timeSent", 0L)

        when (intent.action) {
            SpotifyBroadcastType.PlaybackStateChanged.id -> onPlaybackStateChanged(
                SpotifyPlaybackStateChangedData(
                    intent.getBooleanExtra("playing", false),
                    intent.getIntExtra("playbackPosition", 0),
                    timeSentInMs
                )
            )
            SpotifyBroadcastType.QueueChanged.id -> onQueueChanged(SpotifyQueueChangedData(timeSentInMs))
            SpotifyBroadcastType.MetadataChanged.id -> onMetadataChanged(
                SpotifyMetadataChangedData(
                    PlayableUri(intent.getStringExtra("id")!!),
                    intent.getStringExtra("artist")!!,
                    intent.getStringExtra("album")!!,
                    intent.getStringExtra("track")!!,
                    intent.getIntExtra("length", 0),
                    timeSentInMs
                )
            )
        }
    }

    /**
     * A metadata change intent is sent when a new track starts playing.
     *
     * @param data The data associated with this broadcast.
     */
    public open fun onMetadataChanged(data: SpotifyMetadataChangedData) {
        sendUnregisteredNotificationMessage(data.type.id)
    }

    /**
     * A playback state change is sent whenever the user presses play/pause, or when seeking the track position.
     *
     * @param data The data associated with this broadcast.
     */
    public open fun onPlaybackStateChanged(data: SpotifyPlaybackStateChangedData) {
        sendUnregisteredNotificationMessage(data.type.id)
    }

    /**
     * A queue change is sent whenever the play queue is changed.
     *
     * @param data The data associated with this broadcast.
     */
    public open fun onQueueChanged(data: SpotifyQueueChangedData) {
        sendUnregisteredNotificationMessage(data.type.id)
    }

    private fun sendUnregisteredNotificationMessage(action: String) {
        logToConsole("Unregistered notification $action has no handler.")
    }

    public companion object {
        public const val BaseSpotifyNotificationId: String = "com.spotify.music"
    }
}

/**
 * Broadcast receiver types. These must be turned on manually in the Spotify app settings.
 */
public enum class SpotifyBroadcastType(public val id: String) {
    PlaybackStateChanged("$BaseSpotifyNotificationId.playbackstatechanged"),
    QueueChanged("$BaseSpotifyNotificationId.queuechanged"),
    MetadataChanged("$BaseSpotifyNotificationId.metadatachanged")
}

/**
 * Data from a broadcast event
 *
 * @param type The type of the broadcast event
 */
public abstract class SpotifyBroadcastEventData(public val type: SpotifyBroadcastType)

/**
 * A metadata change intent is sent when a new track starts playing. It uses the intent action com.spotify.music.metadatachanged.
 *
 * @param playableUri A Spotify URI for the track or playable.
 * @param artistName The track artist.
 * @param albumName The album name.
 * @param trackName The track name.
 * @param trackLengthInSec Length of the track, in seconds.
 * @param timeSentInMs When the notification was sent.
 */
public data class SpotifyMetadataChangedData(
    val playableUri: PlayableUri,
    val artistName: String,
    val albumName: String,
    val trackName: String,
    val trackLengthInSec: Int,
    val timeSentInMs: Long
) : SpotifyBroadcastEventData(SpotifyBroadcastType.MetadataChanged)

/**
 * A playback state change is sent whenever the user presses play/pause, or when seeking the track position. It uses the intent action com.spotify.music.playbackstatechanged.
 *
 * @param playing True if playing, false if paused.
 * @param positionInMs The current playback position in milliseconds.
 * @param timeSentInMs When the notification was sent.
 */
public data class SpotifyPlaybackStateChangedData(
    val playing: Boolean,
    val positionInMs: Int,
    val timeSentInMs: Long
) : SpotifyBroadcastEventData(SpotifyBroadcastType.PlaybackStateChanged)

/**
 * A queue change is sent whenever the play queue is changed. It uses the intent action com.spotify.music.queuechanged.
 *
 * @param timeSentInMs When the notification was sent.
 */
public class SpotifyQueueChangedData(
    public val timeSentInMs: Long
) : SpotifyBroadcastEventData(SpotifyBroadcastType.QueueChanged)


================================================
FILE: src/androidMain/kotlin/com/adamratzman/spotify/notifications/SpotifyBroadcastReceiverUtils.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.notifications

import android.app.Activity
import android.content.Context
import android.content.IntentFilter
import androidx.fragment.app.Fragment

/**
 * Register a Spotify broadcast receiver (receiving notifications from the Spotify app) for the specified notification types.
 *
 * This should be used in a [Fragment] or [Activity].
 *
 * Note that "Device Broadcast Status" must be enabled in the Spotify app and the active Spotify device must be the Android
 * device that your app is on to receive notifications.
 *
 * @param receiver An instance of your implementation of [AbstractSpotifyBroadcastReceiver]
 * @param notificationTypes The notification types that you would like to subscribe to.
 */
public fun Context.registerSpotifyBroadcastReceiver(
    receiver: AbstractSpotifyBroadcastReceiver,
    vararg notificationTypes: SpotifyBroadcastType
) {
    val filter = IntentFilter()
    notificationTypes.forEach { filter.addAction(it.id) }

    registerReceiver(receiver, filter)
}


================================================
FILE: src/androidMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.utils

import android.app.Activity
import android.content.Context
import android.util.Log
import android.widget.Toast
import kotlinx.coroutines.runBlocking
import java.net.URLEncoder

internal actual fun String.encodeUrl() = URLEncoder.encode(this, "UTF-8")!!

/**
 * Actual platform that this program is run on.
 */
public actual val currentApiPlatform: Platform = Platform.Android

public actual typealias ConcurrentHashMap<K, V> = java.util.concurrent.ConcurrentHashMap<K, V>

public actual fun <K, V> ConcurrentHashMap<K, V>.asList(): List<Pair<K, V>> = toList()

// safeLet retrieved from: https://stackoverflow.com/a/35522422/6422820
private fun <T1 : Any, T2 : Any, T3 : Any, R : Any> safeLet(p1: T1?, p2: T2?, p3: T3?, block: (T1, T2, T3) -> R?): R? =
    if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null

internal fun toast(context: Context?, message: String?, duration: Int = Toast.LENGTH_SHORT) {
    safeLet(context, message, duration) { safeContext, safeMessage, safeDuration ->
        (safeContext as? Activity)?.runOnUiThread {
            Toast.makeText(safeContext, safeMessage, safeDuration).show()
        }
    }
}

internal fun logToConsole(message: String) {
    Log.i("spotify-web-api-kotlin", message)
}

public actual fun <T> runBlockingOnJvmAndNative(block: suspend () -> T): T {
    return runBlocking { block() }
}


================================================
FILE: src/androidMain/res/layout/spotify_pkce_auth_layout.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/progress_overlay"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:alpha="0.4"
             android:animateLayoutChanges="true"
             android:background="@android:color/black"
             android:clickable="true"
             android:focusable="true"
             android:visibility="gone">
</FrameLayout>


================================================
FILE: src/commonJvmLikeMain/kotlin/com/adamratzman/spotify/javainterop/SpotifyContinuation.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
@file:JvmName("SpotifyContinuation")

package com.adamratzman.spotify.javainterop

import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

/**
 * A [Continuation] wrapper to allow you to directly implement [onSuccess] and [onFailure], when exceptions are hidden
 * on JVM via traditional continuations. **Please use this class as a callback anytime you are using Java code with this library.**
 *
 */
public abstract class SpotifyContinuation<in T> : Continuation<T> {
    /**
     * Invoke a function with the callback [value]
     *
     * @param value The value retrieved from the Spotify API.
     */
    public abstract fun onSuccess(value: T)

    /**
     * Handle exceptions during this API call.
     *
     * @param exception The exception that was thrown during the call.
     */
    public abstract fun onFailure(exception: Throwable)

    override fun resumeWith(result: Result<T>) {
        result.fold(::onSuccess, ::onFailure)
    }

    override val context: CoroutineContext = EmptyCoroutineContext
}


================================================
FILE: src/commonJvmLikeMain/kotlin/com/adamratzman/spotify/utils/DateTimeUtils.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify.utils

import kotlinx.datetime.Instant

/**
 * The current time in milliseconds since UNIX epoch.
 */
public actual fun getCurrentTimeMs(): Long = System.currentTimeMillis()

/**
 * Format date to ISO 8601 format
 */
internal actual fun formatDate(date: Long): String {
    return Instant.fromEpochMilliseconds(date).toString()
}


================================================
FILE: src/commonJvmLikeTest/kotlin/com/adamratzman/spotify/CommonImpl.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify

import com.adamratzman.spotify.http.HttpRequest
import com.adamratzman.spotify.http.HttpResponse
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File

val cacheLocation: String? = System.getenv("RESPONSE_CACHE_DIR")
val shouldRecacheRequests: Boolean = System.getenv("SHOULD_RECACHE_RESPONSES")?.toBoolean() == true

actual fun getTestClientId(): String? = System.getenv("SPOTIFY_CLIENT_ID")
actual fun getTestClientSecret(): String? = System.getenv("SPOTIFY_CLIENT_SECRET")
actual fun getTestRedirectUri(): String? = System.getenv("SPOTIFY_REDIRECT_URI")
actual fun getTestTokenString(): String? = System.getenv("SPOTIFY_TOKEN_STRING")
actual fun isHttpLoggingEnabled(): Boolean = System.getenv("SPOTIFY_LOG_HTTP") == "true"
actual fun arePlayerTestsEnabled(): Boolean = System.getenv("SPOTIFY_ENABLE_PLAYER_TESTS")?.toBoolean() == true
actual fun areLivePkceTestsEnabled(): Boolean = System.getenv("VERBOSE_TEST_ENABLED")?.toBoolean() ?: false

var hasInstantiatedApi: Boolean = false
var backingApi: GenericSpotifyApi? = null

actual suspend fun buildSpotifyApi(testClassQualifiedName: String, testName: String): GenericSpotifyApi? {
    if (!hasInstantiatedApi) {
        backingApi = buildSpotifyApiInternal()
        hasInstantiatedApi = true
    }

    return backingApi;
}

private suspend fun buildSpotifyApiInternal(): GenericSpotifyApi? {
    val clientId = getTestClientId()
    val clientSecret = getTestClientSecret()
    val tokenString = getTestTokenString()
    val logHttp = isHttpLoggingEnabled()

    val optionsCreator: (SpotifyApiOptions.() -> Unit) = {
        this.enableDebugMode = logHttp
        retryOnInternalServerErrorTimes = 0
    }

    return when {
        tokenString?.isNotBlank() == true -> {
            spotifyClientApi {
                credentials {
                    this.clientId = clientId
                    this.clientSecret = clientSecret
                    this.redirectUri = getTestRedirectUri()
                }
                authorization {
                    this.tokenString = tokenString
                }
                options(optionsCreator)
            }.build()
        }

        clientId?.isNotBlank() == true -> {
            spotifyAppApi {
                credentials {
                    this.clientId = clientId
                    this.clientSecret = clientSecret
                }
                options(optionsCreator)
            }.build()
        }

        else -> null
    }
}

object JvmResponseCacher : ResponseCacher {
    override val cachedResponsesDirectoryPath: String = cacheLocation ?: ""
    private val json = Json { prettyPrint = true }
    private val baseDirectory = File(cacheLocation)

    init {
        if (baseDirectory.exists()) baseDirectory.deleteRecursively()
        baseDirectory.mkdirs()
    }

    override fun cacheResponse(
        className: String,
        testName: String,
        responseNumber: Int,
        request: HttpRequest,
        response: HttpResponse
    ) {
        val testDirectory = File(baseDirectory.absolutePath + "/$className/$testName")
        if (!testDirectory.exists()) testDirectory.mkdirs()

        val responseFile = File(testDirectory.absolutePath + "/http_request_$responseNumber.txt")
        if (responseFile.exists()) responseFile.delete()
        responseFile.createNewFile()

        val objToWrite = CachedResponse(
            Request(
                request.url,
                request.method.toString(),
                request.bodyString
            ),
            Response(
                response.responseCode,
                response.headers.associate { it.key to it.value },
                response.body
            )
        )

        responseFile.appendText(json.encodeToString(objToWrite))
    }
}

actual fun getResponseCacher(): ResponseCacher? {
    if (cacheLocation == null || !shouldRecacheRequests) return null
    return JvmResponseCacher
}


================================================
FILE: src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApi.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
@file:Suppress("LeakingThis")

package com.adamratzman.spotify

import com.adamratzman.spotify.SpotifyException.BadRequestException
import com.adamratzman.spotify.endpoints.client.*
import com.adamratzman.spotify.endpoints.pub.*
import com.adamratzman.spotify.http.*
import com.adamratzman.spotify.models.AuthenticationError
import com.adamratzman.spotify.models.Token
import com.adamratzman.spotify.models.TokenValidityResponse
import com.adamratzman.spotify.models.serialization.nonstrictJson
import com.adamratzman.spotify.models.serialization.toObject
import com.adamratzman.spotify.utils.asList
import com.adamratzman.spotify.utils.base64ByteEncode
import kotlinx.serialization.json.Json
import kotlin.jvm.JvmOverloads

/**
 * Represents an instance of the Spotify API client, with common
 * functionality and information between the [SpotifyClientApi] and [SpotifyAppApi]
 * implementations of the API
 *
 * @param clientId The application client id found on the application [dashboard](https://developer.spotify.com/dashboard/applications)
 * @param clientSecret The application client secret found on the application [dashboard](https://developer.spotify.com/dashboard/applications)
 * @param token The access token associated with this API instance
 * @param spotifyApiOptions Configurable Spotify API options.
 *
 * @property search Provides access to the Spotify [search endpoint](https://developer.spotify.com/documentation/web-api/reference/search/search/)
 * @property albums Provides access to Spotify [album endpoints](https://developer.spotify.com/documentation/web-api/reference/albums/)
 * @property browse Provides access to Spotify [browse endpoints](https://developer.spotify.com/documentation/web-api/reference/browse/)
 * @property artists Provides access to Spotify [artist endpoints](https://developer.spotify.com/documentation/web-api/reference/artists/)
 * @property tracks Provides access to Spotify [track endpoints](https://developer.spotify.com/documentation/web-api/reference/tracks/)
 * @property episodes Provides access to Spotify [episode endpoints](https://developer.spotify.com/documentation/web-api/reference/episodes/)
 * @property shows Provides access to Spotify [show endpoints](https://developer.spotify.com/documentation/web-api/reference/shows/)
 * @property markets Provides access to Spotify [market endpoints](https://developer.spotify.com/documentation/web-api/reference/#category-markets)
 */
public sealed class SpotifyApi<T : SpotifyApi<T, B>, B : ISpotifyApiBuilder<T, B>>(
    public val clientId: String?,
    public val clientSecret: String?,
    public var token: Token,
    public var spotifyApiOptions: SpotifyApiOptions
) {
    public var useCache: Boolean = spotifyApiOptions.useCache
        set(value) {
            if (!value) clearCache()

            field = value
        }
    public val expireTime: Long get() = token.expiresAt
    public var runExecutableFunctions: Boolean = true

    public abstract val search: SearchApi
    public abstract val albums: AlbumApi
    public abstract val browse: BrowseApi
    public abstract val artists: ArtistApi
    public abstract val playlists: PlaylistApi
    public abstract val users: UserApi
    public abstract val tracks: TrackApi
    public abstract val following: FollowingApi
    public abstract val episodes: EpisodeApi
    public abstract val shows: ShowApi
    public abstract val markets: MarketsApi

    /**
     * Base url for Spotify web api calls
     */
    internal val spotifyApiBase = "https://api.spotify.com/v1"

    internal val defaultEndpoint get() = tracks

    init {
        spotifyApiOptions.requiredScopes?.let { requiredScopes ->
            val tokenScopes = token.scopes ?: listOf()
            if (!tokenScopes.containsAll(requiredScopes)) {
                val missingScopes = requiredScopes.filter { it !in tokenScopes }

                throw IllegalStateException(
                    "Expected authorized scopes $requiredScopes, but was missing the following scopes: $missingScopes"
                )
            }
        }
    }

    /**
     * Obtain a map of all currently-cached requests
     */
    public fun getCache(): Map<SpotifyRequest, CacheState> =
        endpoints.map { it.cache.cachedRequests.asList() }.flatten().toMap()

    /**
     * Change the current [Token]'s access token
     */
    public fun updateTokenWith(tokenString: String) {
        updateToken {
            accessToken = tokenString
        }
    }

    /**
     * Modify the current [Token] via DSL
     */
    public fun updateToken(modifier: Token.() -> Unit) {
        modifier(token)
    }

    /**
     * A list of all endpoints included in this api type
     */
    public abstract val endpoints: List<SpotifyEndpoint>

    /**
     * If the cache is enabled, clear all stored queries in the cache
     */
    public fun clearCache(): Unit = clearCaches(*endpoints.toTypedArray())

    /**
     * Return a new [SpotifyApiBuilder] with the parameters provided to this api instance
     */
    public abstract fun getApiBuilder(): SpotifyApiBuilder

    /**
     * Return a new [B] with the parameters provided to this api instance
     */
    public abstract fun getApiBuilderDsl(): B

    private fun clearCaches(vararg endpoints: SpotifyEndpoint) {
        endpoints.forEach { it.cache.clear() }
    }

    /**
     * Create a Spotify authorization URL from which client access can be obtained
     *
     * @param scopes The scopes that the application should have access to
     * @param redirectUri The redirect uri specified on the Spotify developer dashboard; where to
     * redirect the browser after authentication
     * @param state This provides protection against attacks such as cross-site request forgery.
     *
     * @return Authorization URL that can be used in a browser
     */
    public fun getAuthorizationUrl(vararg scopes: SpotifyScope, redirectUri: String, state: String? = null): String {
        require(clientId != null)
        return getAuthUrlFull(
            *scopes,
            clientId = clientId,
            redirectUri = redirectUri,
            state = state
        )
    }

    public fun getSpotifyPkceAuthorizationUrl(
        vararg scopes: SpotifyScope,
        redirectUri: String,
        codeChallenge: String,
        state: String? = null
    ): String {
        require(clientId != null)
        return getPkceAuthUrlFull(
            *scopes,
            clientId = clientId,
            redirectUri = redirectUri,
            codeChallenge = codeChallenge,
            state = state
        )
    }

    /**
     * Tests whether the current [token] is actually valid. By default, an endpoint is called *once* to verify
     * validity.
     *
     * @param makeTestRequest Whether to also make an endpoint request to verify authentication.
     *
     * @return [TokenValidityResponse] containing whether this token is valid, and if not, an Exception explaining why
     */
    @JvmOverloads
    public suspend fun isTokenValid(
        makeTestRequest: Boolean = true
    ): TokenValidityResponse {
        if (token.shouldRefresh()) {
            return TokenValidityResponse(
                false,
                SpotifyException.AuthenticationException("Token needs to be refreshed (is it expired?)")
            )
        }
        if (!makeTestRequest) return TokenValidityResponse(true, null)

        return try {
            browse.getAvailableGenreSeeds()
            TokenValidityResponse(true, null)
        } catch (e: Exception) {
            TokenValidityResponse(false, e)
        }
    }

    /**
     * Tests whether the current [token] is actually valid. By default, an endpoint is called *once* to verify
     * validity.
     *
     * @param makeTestRequest Whether to also make an endpoint request to verify authentication.
     *
     * @return [TokenValidityResponse] containing whether this token is valid, and if not, an Exception explaining why
     */
    @JvmOverloads
    public fun isTokenValidRestAction(makeTestRequest: Boolean = true): SpotifyRestAction<TokenValidityResponse> =
        SpotifyRestAction {
            isTokenValid(makeTestRequest)
        }

    /**
     * If the method used to create the [token] supports token refresh and
     * the information in [token] is accurate, attempt to refresh the token
     *
     * @return The old access token if refresh was successful
     * @throws BadRequestException if refresh fails
     * @throws IllegalStateException if [SpotifyApiOptions.refreshTokenProducer] is null
     */
    public suspend fun refreshToken(): Token {
        val oldToken = token
        val refreshedToken = spotifyApiOptions.refreshTokenProducer?.invoke(this)
            ?: throw SpotifyException.ReAuthenticationNeededException(IllegalStateException("The refreshTokenProducer is null."))

        token = refreshedToken
        // Spotify may not provide a new refresh token
        if (token.refreshToken == null) token.refreshToken = oldToken.refreshToken
        
        spotifyApiOptions.onTokenRefresh?.invoke(this@SpotifyApi)
        spotifyApiOptions.afterTokenRefresh?.invoke(this@SpotifyApi)
        
        return oldToken
    }

    /**
     * If the method used to create the [token] supports token refresh and
     * the information in [token] is accurate, attempt to refresh the token
     *
     * @return The old access token if refresh was successful
     * @throws BadRequestException if refresh fails
     * @throws IllegalStateException if [SpotifyApiOptions.refreshTokenProducer] is null
     */
    public fun refreshTokenRestAction(): SpotifyRestAction<Token> = SpotifyRestAction { refreshToken() }

    public companion object {
        internal suspend fun testTokenValidity(api: GenericSpotifyApi) {
            if (!api.isTokenValid().isValid) {
                try {
                    api.refreshToken()
                } catch (e: BadRequestException) {
                    throw SpotifyException.AuthenticationException(
                        "Invalid token and refresh token supplied. Cannot refresh to a fresh token.",
                        e
                    )
                }
            }
        }

        /*
            Builder tools
         */

        /**
         * Get the authorization url for the provided [clientId] and [redirectUri] application settings, when attempting to authorize with
         * specified [scopes]
         *
         * @param scopes Spotify scopes the api instance should be able to access for the user
         * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param state This provides protection against attacks such as cross-site request forgery.
         */
        public fun getAuthUrlFull(
            vararg scopes: SpotifyScope,
            clientId: String,
            redirectUri: String,
            isImplicitGrantFlow: Boolean = false,
            shouldShowDialog: Boolean = false,
            state: String? = null
        ): String {
            return "https://accounts.spotify.com/authorize/?client_id=$clientId" +
                    "&response_type=${if (isImplicitGrantFlow) "token" else "code"}" +
                    "&redirect_uri=$redirectUri" +
                    (state?.let { "&state=$it" } ?: "") +
                    if (scopes.isEmpty()) {
                        ""
                    } else {
                        "&scope=${scopes.joinToString("%20") { it.uri }}" +
                                if (shouldShowDialog) "&show_dialog=$shouldShowDialog" else ""
                    }
        }

        /**
         * Get the PKCE authorization url for the provided [clientId] and [redirectUri] application settings, when attempting to authorize with
         * specified [scopes]
         *
         * @param scopes Spotify scopes the api instance should be able to access for the user
         * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param codeChallenge The code challenge corresponding to your codeVerifier. **It is highly recommend to use
         * [getSpotifyPkceCodeChallenge] to get the code challenge from a code verifier (only available for JVM/Android).**
         * @param state This provides protection against attacks such as cross-site request forgery.
         */
        public fun getPkceAuthUrlFull(
            vararg scopes: SpotifyScope,
            clientId: String,
            redirectUri: String,
            codeChallenge: String,
            state: String? = null
        ): String {
            return "https://accounts.spotify.com/authorize/?client_id=$clientId" +
                    "&response_type=code" +
                    "&redirect_uri=$redirectUri" +
                    "&code_challenge_method=S256" +
                    "&code_challenge=$codeChallenge" +
                    (state?.let { "&state=$it" } ?: "") +
                    if (scopes.isEmpty()) "" else "&scope=${scopes.joinToString("%20") { it.uri }}"
        }

        /**
         *
         * Get an application token (can only access public methods) that can be used to instantiate a new [SpotifyAppApi]
         *
         * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param api The Spotify Api instance, or null if one doesn't exist yet
         * @param json The json instance that will deserialize the response.
         */
        public suspend fun getCredentialedToken(
            clientId: String,
            clientSecret: String,
            api: GenericSpotifyApi?,
            json: Json = api?.spotifyApiOptions?.json ?: Json.Default
        ): Token {
            val response = executeTokenRequest(
                HttpRequest(
                    "https://accounts.spotify.com/api/token",
                    HttpRequestMethod.POST,
                    mapOf("grant_type" to "client_credentials"),
                    null,
                    "application/x-www-form-urlencoded",
                    listOf(),
                    api
                ),
                clientId,
                clientSecret
            )

            if (response.responseCode / 200 == 1) return response.body.toObject(Token.serializer(), null, json)

            throw BadRequestException(response.body.toObject(AuthenticationError.serializer(), null, json))
        }

        /**
         *
         * Get an application token (can only access public methods) that can be used to instantiate a new [SpotifyAppApi]
         *
         * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param api The Spotify Api instance, or null if one doesn't exist yet
         * @param json The json instance that will deserialize the response.
         */
        public fun getCredentialedTokenRestAction(
            clientId: String,
            clientSecret: String,
            api: GenericSpotifyApi?,
            json: Json = api?.spotifyApiOptions?.json ?: Json.Default
        ): SpotifyRestAction<Token> = SpotifyRestAction { getCredentialedToken(clientId, clientSecret, api, json) }
    }
}

/**
 * An API instance created with application credentials, not through
 * client authentication
 */
public class SpotifyAppApi internal constructor(
    clientId: String?,
    clientSecret: String?,
    token: Token,
    enableDefaultTokenRefreshProducerIfNoneExists: Boolean = true,
    spotifyApiOptions: SpotifyApiOptions
) : SpotifyApi<SpotifyAppApi, SpotifyAppApiBuilder>(
    clientId,
    clientSecret,
    token,
    spotifyApiOptions.apply {
        if (enableDefaultTokenRefreshProducerIfNoneExists && refreshTokenProducer == null) {
            refreshTokenProducer = defaultAppApiTokenRefreshProducer
        }
    }
) {
    override val search: SearchApi = SearchApi(this)
    override val albums: AlbumApi = AlbumApi(this)
    override val browse: BrowseApi = BrowseApi(this)
    override val artists: ArtistApi = ArtistApi(this)
    override val tracks: TrackApi = TrackApi(this)
    override val episodes: EpisodeApi = EpisodeApi(this)
    override val shows: ShowApi = ShowApi(this)
    override val markets: MarketsApi = MarketsApi(this)

    /**
     * Provides access to **public** Spotify [playlist endpoints](https://developer.spotify.com/documentation/web-api/reference/playlists/)
     */
    override val playlists: PlaylistApi = PlaylistApi(this)

    /**
     * Provides access to **public** Spotify [user information](https://developer.spotify.com/documentation/web-api/reference/users-profile/get-users-profile/)
     */
    override val users: UserApi = UserApi(this)

    /**
     * Provides access to **public** playlist [follower information](https://developer.spotify.com/documentation/web-api/reference/follow/check-user-following-playlist/)
     */
    override val following: FollowingApi = FollowingApi(this)

    override val endpoints: List<SpotifyEndpoint>
        get() = listOf(
            search,
            albums,
            browse,
            artists,
            playlists,
            users,
            tracks,
            following
        )

    override fun getApiBuilder(): SpotifyApiBuilder = SpotifyApiBuilder(
        clientId,
        clientSecret,
        null
    ).apply { useCache(useCache) }

    override fun getApiBuilderDsl(): SpotifyAppApiBuilder = spotifyAppApi {
        credentials {
            clientId = this@SpotifyAppApi.clientId
            clientSecret = this@SpotifyAppApi.clientSecret
        }

        useCache = this@SpotifyAppApi.useCache
    }

    public companion object {
        private val defaultAppApiTokenRefreshProducer: suspend (SpotifyApi<*, *>) -> Token = { api ->
            require(api.clientId != null && api.clientSecret != null) { "Either the client id or the client secret is not set" }

            getCredentialedToken(api.clientId, api.clientSecret, api, api.spotifyApiOptions.json)
        }
    }
}

/**
 * An API instance created through client authentication, with access to private information
 * managed through the scopes exposed in [token]
 */
public open class SpotifyClientApi(
    clientId: String?,
    clientSecret: String?,
    public var redirectUri: String?,
    token: Token,
    public val usesPkceAuth: Boolean,
    enableDefaultTokenRefreshProducerIfNoneExists: Boolean,
    spotifyApiOptions: SpotifyApiOptions
) : SpotifyApi<SpotifyClientApi, SpotifyClientApiBuilder>(
    clientId,
    clientSecret,
    token,
    spotifyApiOptions.apply {
        if (enableDefaultTokenRefreshProducerIfNoneExists && refreshTokenProducer == null) {
            refreshTokenProducer = defaultClientApiTokenRefreshProducer
        }
    }
) {
    public constructor(
        clientId: String?,
        clientSecret: String?,
        token: Token,
        spotifyApiOptions: SpotifyApiOptions
    ) : this(
        clientId,
        clientSecret,
        null,
        token,
        false,
        false,
        spotifyApiOptions
    )

    override val albums: AlbumApi = AlbumApi(this)
    override val browse: BrowseApi = BrowseApi(this)
    override val artists: ArtistApi = ArtistApi(this)
    override val tracks: TrackApi = TrackApi(this)
    override val search: SearchApi = SearchApi(this)
    override val markets: MarketsApi = MarketsApi(this)

    override val episodes: ClientEpisodeApi = ClientEpisodeApi(this)
    override val shows: ClientShowApi = ClientShowApi(this)

    /**
     * Provides access to [endpoints](https://developer.spotify.com/documentation/web-api/reference/playlists/) for retrieving
     * information about a user’s playlists and for managing a user’s playlists.
     * *Superset of [PlaylistApi]*
     */
    override val playlists: ClientPlaylistApi = ClientPlaylistApi(this)

    /**
     * Provides access to [endpoints](https://developer.spotify.com/documentation/web-api/reference/users-profile/) for
     * retrieving information about a user’s profile.
     * *Superset of [UserApi]*
     */
    override val users: ClientProfileApi = ClientProfileApi(this)

    /**
     * Provides access to [endpoints](https://developer.spotify.com/documentation/web-api/reference/follow/) for managing
     * the artists, users, and playlists that a Spotify user follows.
     * *Superset of [FollowingApi]*
     */
    override val following: ClientFollowingApi = ClientFollowingApi(this)

    /**
     * Provides access to [endpoints](https://developer.spotify.com/documentation/web-api/reference/personalization/) for
     * retrieving information about the user’s listening habits.

     */
    public val personalization: ClientPersonalizationApi = ClientPersonalizationApi(this)

    /**
     * Provides access to [endpoints](https://developer.spotify.com/documentation/web-api/reference/library/) for
     * retrieving information about, and managing, tracks that the current user has saved in their “Your Music” library.
     */
    public val library: ClientLibraryApi = ClientLibraryApi(this)

    /**
     * Provides access to the **beta** [player api](https://developer.spotify.com/documentation/web-api/reference/player/),
     * including track playing and pausing endpoints.
     *
     * Please consult the [usage guide](https://developer.spotify.com/documentation/web-api/guides/using-connect-web-api/) before
     * calling any endpoint in this api.
     *
     * **These endpoints may break at any time.**
     */
    public val player: ClientPlayerApi = ClientPlayerApi(this)

    private var userIdBacking: String? = null

    private suspend fun initiatizeUserIdBacking(): String {
        userIdBacking = users.getClientProfile().id
        return userIdBacking!!
    }

    /**
     * The Spotify user id to which the api instance is connected
     */
    public suspend fun getUserId(): String =
        if (userIdBacking != null) userIdBacking!! else initiatizeUserIdBacking()

    /**
     * The Spotify user id to which the api instance is connected
     */
    public fun getUserIdRestAction(): SpotifyRestAction<String> = SpotifyRestAction { getUserId() }

    /**
     * Stop all automatic functions like refreshToken or clearCache and shut down the scheduled
     * executor
     * */
    public fun shutdown() {
        runExecutableFunctions = false
    }

    override val endpoints: List<SpotifyEndpoint>
        get() = listOf(
            search,
            albums,
            browse,
            artists,
            playlists,
            users,
            tracks,
            following,
            personalization,
            library,
            player
        )

    override fun getApiBuilder(): SpotifyApiBuilder = SpotifyApiBuilder(
        clientId,
        clientSecret,
        redirectUri
    ).apply {
        redirectUri(redirectUri)
        useCache(useCache)
    }

    override fun getApiBuilderDsl(): SpotifyClientApiBuilder = spotifyClientApi {
        credentials {
            clientId = this@SpotifyClientApi.clientId
            clientSecret = this@SpotifyClientApi.clientSecret
            redirectUri = this@SpotifyClientApi.redirectUri
        }

        useCache = this@SpotifyClientApi.useCache
    }

    /**
     * Create a Spotify authorization URL from which client access can be obtained
     *
     * @param scopes The scopes that the application should have access to
     *
     * @return Authorization URL that can be used in a browser
     */
    public fun getAuthorizationUrl(vararg scopes: SpotifyScope, state: String? = null): String {
        require(clientId != null && clientSecret != null) { "Either the client id or the client secret is not set" }
        return redirectUri?.let { getAuthUrlFull(*scopes, clientId = clientId, redirectUri = it, state = state) }
            ?: throw IllegalArgumentException("The redirect uri must be set")
    }

    /**
     * Whether the current access token allows access to scope [scope]
     */
    public suspend fun hasScope(scope: SpotifyScope): Boolean? = hasScopes(scope)

    /**
     * Whether the current access token allows access to scope [scope]
     */
    public fun hasScopeRestAction(scope: SpotifyScope): SpotifyRestAction<Boolean?> =
        SpotifyRestAction { hasScope(scope) }

    /**
     * Whether the current access token allows access to all of the provided scopes
     */
    public suspend fun hasScopes(scope: SpotifyScope, vararg scopes: SpotifyScope): Boolean? =
        if (token.scopes == null) {
            null
        } else {
            isTokenValid(false).isValid &&
                    token.scopes?.contains(scope) == true &&
                    scopes.all { token.scopes?.contains(it) == true }
        }

    /**
     * Whether the current access token allows access to all of the provided scopes
     */
    public fun hasScopesRestAction(scope: SpotifyScope, vararg scopes: SpotifyScope): SpotifyRestAction<Boolean?> =
        SpotifyRestAction {
            hasScopes(scope, *scopes)
        }

    public companion object {
        private val defaultClientApiTokenRefreshProducer: suspend (GenericSpotifyApi) -> Token = { api ->
            api as SpotifyClientApi

            require(api.clientId != null) { "The client id is not set" }

            refreshSpotifyClientToken(api.clientId, api.clientSecret, api.token.refreshToken, api.usesPkceAuth)
        }
    }
}

/**
 * An API instance created through implicit grant flow, with access to private information
 * managed through the scopes exposed in [token]. [token] is not refreshable and is only accessible for limited time.
 */
public class SpotifyImplicitGrantApi(
    clientId: String?,
    token: Token,
    spotifyApiOptions: SpotifyApiOptions
) : SpotifyClientApi(
    clientId,
    null,
    token,
    spotifyApiOptions
)

/**
 * Represents a generic instance of the Spotify API client, with common functionality and information between
 * implementations of the API
 */
public typealias GenericSpotifyApi = SpotifyApi<*, *>

/**
 *
 * Get an application token (can only access public methods) that can be used to instantiate a new [SpotifyAppApi]
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param api The Spotify Api instance, or null if one doesn't exist yet
 * @param json The json instance that will deserialize the response.
 */
@Deprecated("Moved", ReplaceWith("SpotifyApi.getCredentialedToken"))
public suspend fun getCredentialedToken(
    clientId: String,
    clientSecret: String,
    api: GenericSpotifyApi?,
    json: Json = api?.spotifyApiOptions?.json ?: Json.Default
): Token = SpotifyApi.getCredentialedToken(clientId, clientSecret, api, json)

internal suspend fun executeTokenRequest(
    httpRequest: HttpRequest,
    clientId: String,
    clientSecret: String
): HttpResponse {
    return httpRequest.execute(
        listOf(
            HttpHeader(
                "Authorization",
                "Basic ${"$clientId:$clientSecret".base64ByteEncode()}"
            )
        )
    )
}

/**
 * Refresh a Spotify client token
 *
 * @param clientId The Spotify application client id.
 * @param clientSecret The Spotify application client secret (not needed for PKCE).
 * @param refreshToken The refresh token.
 * @param usesPkceAuth Whether this token was created using PKCE auth or not.
 */
public suspend fun refreshSpotifyClientToken(
    clientId: String,
    clientSecret: String?,
    refreshToken: String?,
    usesPkceAuth: Boolean
): Token {
    fun getDefaultClientApiTokenBody(): Map<String, String?> {
        val map = mutableMapOf(
            "grant_type" to "refresh_token",
            "refresh_token" to refreshToken
        )

        if (usesPkceAuth) map += "client_id" to clientId

        return map
    }

    val response = if (!usesPkceAuth) {
        require(clientSecret != null) { "The client secret is not set" }
        executeTokenRequest(
            HttpRequest(
                "https://accounts.spotify.com/api/token",
                HttpRequestMethod.POST,
                getDefaultClientApiTokenBody(),
                null,
                "application/x-www-form-urlencoded",
                listOf(),
                null
            ),
            clientId,
            clientSecret
        )
    } else {
        HttpRequest(
            "https://accounts.spotify.com/api/token",
            HttpRequestMethod.POST,
            getDefaultClientApiTokenBody(),
            null,
            "application/x-www-form-urlencoded",
            listOf(),
            null
        ).execute()
    }

    return if (response.responseCode in 200..399) {
        response.body.toObject(Token.serializer(), null, nonstrictJson)
    } else {
        throw BadRequestException(
            response.body.toObject(
                AuthenticationError.serializer(),
                null,
                nonstrictJson
            )
        )
    }
}

/**
 * Refresh a Spotify client token
 *
 * @param clientId The Spotify application client id.
 * @param clientSecret The Spotify application client secret (not needed for PKCE).
 * @param refreshToken The refresh token.
 * @param usesPkceAuth Whether this token was created using PKCE auth or not.
 */
public fun refreshSpotifyClientTokenRestAction(
    clientId: String,
    clientSecret: String?,
    refreshToken: String?,
    usesPkceAuth: Boolean
): SpotifyRestAction<Token> =
    SpotifyRestAction { refreshSpotifyClientToken(clientId, clientSecret, refreshToken, usesPkceAuth) }


================================================
FILE: src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt
================================================
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
package com.adamratzman.spotify

import com.adamratzman.spotify.SpotifyApi.Companion.getCredentialedToken
import com.adamratzman.spotify.http.HttpRequest
import com.adamratzman.spotify.http.HttpRequestMethod
import com.adamratzman.spotify.http.HttpResponse
import com.adamratzman.spotify.models.Token
import com.adamratzman.spotify.models.serialization.nonstrictJson
import com.adamratzman.spotify.models.serialization.toObject
import com.adamratzman.spotify.utils.urlEncodeBase64String
import com.soywiz.krypto.SHA256
import io.ktor.client.plugins.ServerResponseException
import io.ktor.utils.io.core.toByteArray
import kotlinx.coroutines.CancellationException
import kotlinx.serialization.json.Json

// Kotlin DSL builders and top-level utilities

// ==============================================

// Get Spotify client authorization url
/**
 * Get the authorization url for the provided [clientId] and [redirectUri] application settings, when attempting to authorize with
 * specified [scopes]
 *
 * @param scopes Spotify scopes the api instance should be able to access for the user
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param isImplicitGrantFlow Whether the authorization url should be for the Implicit Grant flow, otherwise for Authorization Code flo
 * @param shouldShowDialog If [isImplicitGrantFlow] is true, whether or not to force the user to approve the app again if they’ve already done so.
 * @param state This provides protection against attacks such as cross-site request forgery.
 */
public fun getSpotifyAuthorizationUrl(
    vararg scopes: SpotifyScope,
    clientId: String,
    redirectUri: String,
    isImplicitGrantFlow: Boolean = false,
    shouldShowDialog: Boolean = false,
    state: String? = null
): String {
    return SpotifyApi.getAuthUrlFull(
        *scopes,
        clientId = clientId,
        redirectUri = redirectUri,
        isImplicitGrantFlow = isImplicitGrantFlow,
        shouldShowDialog = shouldShowDialog,
        state = state
    )
}

/**
 * Get the PKCE authorization url for the provided [clientId] and [redirectUri] application settings, when attempting to authorize with
 * specified [scopes]
 *
 * @param scopes Spotify scopes the api instance should be able to access for the user
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param state This provides protection against attacks such as cross-site request forgery.
 * @param codeChallenge In order to generate the code challenge, your app should hash the code verifier using the SHA256 algorithm.
 * Then, base64url encode the hash that you generated.
 *
 */
public fun getSpotifyPkceAuthorizationUrl(
    vararg scopes: SpotifyScope,
    clientId: String,
    redirectUri: String,
    codeChallenge: String,
    state: String? = null
): String {
    return SpotifyApi.getPkceAuthUrlFull(
        *scopes,
        clientId = clientId,
        redirectUri = redirectUri,
        codeChallenge = codeChallenge,
        state = state
    )
}

/**
 * A utility to get the pkce code challenge for a corresponding code verifier. Only available on JVM/Android
 */
public fun getSpotifyPkceCodeChallenge(codeVerifier: String): String {
    if (codeVerifier.length !in 43..128) throw IllegalArgumentException("Code verifier must be between 43 and 128 characters long")
    val sha256 = SHA256.digest(codeVerifier.toByteArray()).base64
    return sha256.urlEncodeBase64String()
}

// ==============================================

// Implicit grant builder
/*
 ____________________________
/ This is Implicit Grant     \
\ authorization              /
 ----------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
 */

/**
 * Instantiate a new [SpotifyImplicitGrantApi] using a Spotify [clientId], and [token] retrieved from the implicit
 * grant flow.
 *
 * Use case: I have a token obtained after implicit grant authorization.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param token Token created from the hash response in the implicit grant callback
 *
 * @return [SpotifyImplicitGrantApi] that can immediately begin making calls
 */
public fun spotifyImplicitGrantApi(
    clientId: String?,
    token: Token
): SpotifyImplicitGrantApi = SpotifyImplicitGrantApi(
    clientId,
    token,
    SpotifyApiOptions()
)

/**
 * Instantiate a new [SpotifyImplicitGrantApi] using a Spotify [clientId], and [token] retrieved from the implicit
 * grant flow.
 *
 * Use case: I have a token obtained after implicit grant authorization.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param token Token created from the hash response in the implicit grant callback
 * @param block Block to set API options
 *
 * @return [SpotifyImplicitGrantApi] that can immediately begin making calls
 */
public fun spotifyImplicitGrantApi(
    clientId: String?,
    token: Token,
    block: SpotifyApiOptions.() -> Unit
): SpotifyImplicitGrantApi = SpotifyImplicitGrantApi(
    clientId,
    token,
    SpotifyApiOptions().apply(block)
)

// App Api builders

/*
 ____________________________
/ This is Client Credentials \
\ authorization              /
 ----------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
 */

/**
 * Instantiate a new [SpotifyAppApiBuilder] using a Spotify [clientId] and [clientSecret].
 *
 * Use case: I am using the client credentials flow.
 * I only need access to public Spotify API endpoints, might have an existing token,
 * and might want to deal with advanced configuration.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
 *
 * @return Configurable [SpotifyAppApiBuilder] that, when built, creates a new [SpotifyAppApi]
 */
public fun spotifyAppApi(
    clientId: String,
    clientSecret: String
): SpotifyAppApiBuilder = SpotifyAppApiBuilder().apply {
    credentials {
        this.clientId = clientId
        this.clientSecret = clientSecret
    }
}

/**
 * Instantiate a new [SpotifyAppApiBuilder] using a Spotify [clientId] and [clientSecret], with the ability to configure
 * the api settings by providing a builder initialization [block]
 *
 * Use case: I am using the client credentials flow.
 * I only need access to public Spotify API endpoints, might have an existing token,
 * and might want to deal with advanced configuration.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param block Api settings block
 *
 * @return Configurable [SpotifyAppApiBuilder] that, when built, creates a new [SpotifyAppApi]
 */
public fun spotifyAppApi(
    clientId: String,
    clientSecret: String,
    block: SpotifyAppApiBuilder.() -> Unit = {}
): SpotifyAppApiBuilder = SpotifyAppApiBuilder().apply(block).apply {
    credentials {
        this.clientId = clientId
        this.clientSecret = clientSecret
    }
}

/**
 * Instantiate a new [SpotifyAppApiBuilder] using a [Token]
 *
 * Use case: I am using the client credentials flow.
 * I only need access to public Spotify API endpoints, I have an existing token,
 * and I don't want to deal with advanced configuration.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param authorization A [SpotifyUserAuthorization] that must contain one of the following: authorization code (preferred),
 * access token string (tokenString), [Token] object, **and** that may contain a refresh token (preferred)
 * with which to refresh the access token
 * @param block Override default API options such as the cache limit
 *
 * @return Configurable [SpotifyAppApiBuilder] that, when built, creates a new [SpotifyAppApi]
 */
public fun spotifyAppApi(
    clientId: String?,
    clientSecret: String?,
    authorization: SpotifyUserAuthorization,
    block: SpotifyApiOptions.() -> Unit = {}
): SpotifyAppApiBuilder = SpotifyAppApiBuilder().apply {
    credentials {
        this.clientId = clientId
        this.clientSecret = clientSecret
    }
    authorization(authorization)
    options(block)
}

/**
 * Instantiate a new [SpotifyAppApiBuilder] using a [Token]
 *
 * Use case: I am using the client credentials flow.
 * I only need access to public Spotify API endpoints, I have an existing token,
 * and I don't want to deal with advanced configuration.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param token Build the API using an existing token.
 * @param block Override default API options such as the cache limit
 *
 * @return Configurable [SpotifyAppApiBuilder] that, when built, creates a new [SpotifyAppApi]
 */
public fun spotifyAppApi(
    clientId: String?,
    clientSecret: String?,
    token: Token,
    block: SpotifyApiOptions.() -> Unit = {}
): SpotifyAppApiBuilder = SpotifyAppApiBuilder().apply {
    credentials {
        this.clientId = clientId
        this.clientSecret = clientSecret
    }

    authorization {
        this.token = token
    }

    options(block)
}

/**
 * Instantiate a new [SpotifyAppApiBuilder] by providing a builder initialization [block].
 *
 * **Note**: You **must** provide your app credentials in the [SpotifyAppApiBuilder.credentials] block
 *
 * Use case: I am using the client credentials flow.
 * I only need access to public Spotify API endpoints, and I want to use the [SpotifyAppApiBuilder] DSL
 * to configure everything myself.
 *
 * @param block Api settings block
 *
 * @return Configurable [SpotifyAppApiBuilder] that, when built, creates a new [SpotifyAppApi]
 */
public fun spotifyAppApi(block: SpotifyAppApiBuilder.() -> Unit): SpotifyAppApiBuilder =
    SpotifyAppApiBuilder().apply(block)

// Client Api Builders
/*
 ____________________________
/ This is Authorization Code \
\ authorization              /
 ----------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
 */

/**
 * Instantiate a new [SpotifyClientApiBuilder] using a Spotify [clientId], [clientSecret], and [redirectUri].
 *
 * **Note**: If trying to build [SpotifyClientApi], you **must** provide client authorization in the [SpotifyClientApiBuilder.authorization]
 * block
 *
 * Use case: I am using the client authorization flow.
 * I want access to both public and client Spotify API endpoints, and I want
 * to configure authorization and other settings myself.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
 *
 * @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
 */
public fun spotifyClientApi(
    clientId: String,
    clientSecret: String,
    redirectUri: String
): SpotifyClientApiBuilder = SpotifyClientApiBuilder().apply {
    credentials {
        this.clientId = clientId
        this.clientSecret = clientSecret
        this.redirectUri = redirectUri
    }
}

/**
 * Instantiate a new [SpotifyClientApiBuilder] using a Spotify [clientId], [clientSecret], and [redirectUri], with the ability to configure
 * the api settings by providing a builder initialization [block]
 *
 * **Note**: If trying to build [SpotifyClientApi], you **must** provide client authorization in the [SpotifyClientApiBuilder.authorization]
 * block
 *
 * Use case: I am using the client authorization flow.
 * I want access to both public and client Spotify API endpoints, and I want
 * to configure authorization and other settings myself.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param block Api settings block
 *
 * @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
 */
public fun spotifyClientApi(
    clientId: String,
    clientSecret: String,
    redirectUri: String,
    block: SpotifyClientApiBuilder.() -> Unit
): SpotifyClientApiBuilder = SpotifyClientApiBuilder().apply(block).apply {
    credentials {
        this.clientId = clientId
        this.clientSecret = clientSecret
        this.redirectUri = redirectUri
    }
}

/**
 * Instantiate a new [SpotifyClientApiBuilder] using a Spotify [clientId], [clientSecret], and [redirectUri],
 * with an existing [SpotifyUserAuthorization].
 *
 * Use case: I am using the client authorization flow.
 * I want access to both public and client Spotify API endpoints and I want to configure [authorization]
 * and [block] without using the DSL.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param authorization A [SpotifyUserAuthorization] that must contain one of the following: authorization code (preferred),
 * access token string (tokenString), [Token] object, **and** that may contain a refresh token (preferred)
 * with which to refresh the access token
 * @param block Override default API options such as the cache limit
 *
 * @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
 */
public fun spotifyClientApi(
    clientId: String?,
    clientSecret: String?,
    redirectUri: String?,
    authorization: SpotifyUserAuthorization,
    block: SpotifyApiOptions.() -> Unit = {}
): SpotifyClientApiBuilder = SpotifyClientApiBuilder().apply {
    credentials {
        this.clientId = clientId
        this.clientSecret = clientSecret
        this.redirectUri = redirectUri
    }
    authorization(authorization)
    options(block)
}

/**
 * Instantiate a new [SpotifyClientApiBuilder] using a Spotify [clientId], [clientSecret], and [redirectUri],
 * with an existing [SpotifyUserAuthorization].
 *
 * Use case: I am using the client authorization flow.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param token Build the API using an existing token.
 * @param block Override default API options such as the cache limit
 *
 * @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
 */
public fun spotifyClientApi(
    clientId: String?,
    clientSecret: String?,
    redirectUri: String?,
    token: Token,
    block: SpotifyApiOptions.() -> Unit = {}
): SpotifyClientApiBuilder = SpotifyClientApiBuilder().apply {
    credentials {
        this.clientId = clientId
        this.clientSecret = clientSecret
        this.redirectUri = redirectUri
    }

    authorization {
        this.token = token
    }

    options(block)
}

/**
 * Instantiate a new [SpotifyClientApiBuilder] by providing a builder initialization [block]
 *
 * **Note**: If trying to build [SpotifyClientApi], you **must** provide client authorization in the [SpotifyClientApiBuilder.authorization]
 * block
 *
 * Use case: I am using the client authorization flow.
 * I want access to both public and client Spotify API endpoints and I want to handle configuration
 * via the DSL myself.
 *
 * @param block Api settings block
 *
 * @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
 */
public fun spotifyClientApi(block: SpotifyClientApiBuilder.() -> Unit): SpotifyClientApiBuilder =
    SpotifyClientApiBuilder().apply(block)

// PKCE Client Api Builders
/*
 ____________________________
/ This is Authorization Code \
\ authorization (PKCE)       /
 ----------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
 */

/**
 * Instantiate a new [SpotifyClientApiBuilder]. This is for **PKCE authorization**.
 *
 * Use case: I am using the PKCE client authorization flow.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param authorization A [SpotifyUserAuthorization] that must contain one of the following: authorization code (preferred),
 * access token string (tokenString), [Token] object, **and** that may contain a refresh token (preferred)
 * with which to refresh the access token. Retrieved after PKCE client authorization flow. **You must provide a code verifier (plaintext).
 * @param block Override default API options such as the cache limit
 *
 * @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
 */
public fun spotifyClientPkceApi(
    clientId: String?,
    redirectUri: String?,
    authorization: SpotifyUserAuthorization,
    block: SpotifyApiOptions.() -> Unit = {}
): SpotifyClientApiBuilder = SpotifyClientApiBuilder().apply {
    credentials {
        this.clientId = clientId
        this.redirectUri = redirectUri
    }

    authorization(authorization)
    options(block)

    usesPkceAuth = true
}

/**
 * Instantiate a new [SpotifyClientApiBuilder]. This is for **PKCE authorization**.
 *
 * Use case: I am using the PKCE client authorization flow.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param pkceCodeVerifier The code verifier generated that the client authenticated with (using its code challenge)
 * @param authorizationCode Only available when building [SpotifyClientApi]. Spotify auth code
 *
 * @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
 */
public fun spotifyClientPkceApi(
    clientId: String?,
    redirectUri: String?,
    authorizationCode: String,
    pkceCodeVerifier: String
): SpotifyClientApiBuilder = SpotifyClientApiBuilder().apply {
    credentials {
        this.clientId = clientId
        this.redirectUri = redirectUri
    }

    authorization {
        this.authorizationCode = authorizationCode
        this.pkceCodeVerifier = pkceCodeVerifier
    }

    usesPkceAuth = true
}

/**
 * Instantiate a new [SpotifyClientApiBuilder]. This is for **PKCE authorization**.
 *
 * Use case: I am using the PKCE client authorization flow.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param pkceCodeVerifier The code verifier generated that the client authenticated with (using its code challenge)
 * @param authorizationCode Only available when building [SpotifyClientApi]. Spotify auth code
 * @param block Override default API options such as the cache limit
 *
 * @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
 */
public fun spotifyClientPkceApi(
    clientId: String?,
    redirectUri: String?,
    authorizationCode: String,
    pkceCodeVerifier: String,
    block: SpotifyApiOptions.() -> Unit
): SpotifyClientApiBuilder = SpotifyClientApiBuilder().apply {
    credentials {
        this.clientId = clientId
        this.redirectUri = redirectUri
    }

    authorization {
        this.authorizationCode = authorizationCode
        this.pkceCodeVerifier = pkceCodeVerifier
    }
    options(block)

    usesPkceAuth = true
}

/**
 * Instantiate a new [SpotifyClientApiBuilder]. This is for **PKCE authorization**.
 *
 * Use case: I am using the PKCE client authorization flow.
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param token Build the API using an existing token.
 * @param block Override default API options such as the cache limit
 *
 * @return Configurable [SpotifyClientApiBuilder] that, when built, creates a new [SpotifyClientApi]
 */
public fun spotifyClientPkceApi(
    clientId: String?,
    redirectUri: String?,
    token: Token,
    block: SpotifyApiOptions.() -> Unit = {}
): SpotifyClientApiBuilder = SpotifyClientApiBuilder().apply {
    credentials {
        this.clientId = clientId
        this.redirectUri = redirectUri
    }

    authorization {
        this.token = token
    }
    options(block)

    usesPkceAuth = true
}

/**
 *  Spotify API builder
 */
public class SpotifyApiBuilder(
    private var clientId: String?,
    private var clientSecret: String?,
    private var redirectUri: String?
) {
    /**
     * Allows you to authenticate a [SpotifyClientApi] with an authorization code
     * or build [SpotifyApi] using a refresh token
     */
    public var authorization: SpotifyUserAuthorization = SpotifyUserAuthorization()

    /**
     * Allows you to override default values for caching, token refresh, and logging
     */
    public var options: SpotifyApiOptions = SpotifyApiOptions()

    /**
     * After API creation, set whether to test whether the token is valid by performing a lightweight request
     */
    public fun testTokenValidity(testTokenValidity: Boolean): SpotifyApiBuilder =
        apply { this.options.testTokenValidity = testTokenValidity }

    /**
     * Allows you to set the default amount of objects to retrieve in one request
     */
    public fun defaultLimit(defaultLimit: Int): SpotifyApiBuilder = apply { this.options.defaultLimit = defaultLimit }

    /**
     * Set the application client id
     */
    public fun clientId(clientId: String): SpotifyApiBuilder = apply { this.clientId = clientId }

    /**
     * Set the application client secret
     */
    public fun clientSecret(clientSecret: String): SpotifyApiBuilder = apply { this.clientSecret = clientSecret }

    /**
     * Set whether to cache requests. Default: true
     */
    public fun useCache(useCache: Boolean): SpotifyApiBuilder = apply { this.options.useCache = useCache }

    /**
     *  Set the maximum allowed amount of cached requests at one time. Null means no limit
     */
    public fun cacheLimit(cacheLimit: Int?): SpotifyApiBuilder = apply { this.options.cacheLimit = cacheLimit }

    /**
     * Set the application [redirect uri](https://developer.spotify.com/documentation/general/guides/authorization-guide/)
     */
    public fun redirectUri(redirectUri: String?): SpotifyApiBuilder = apply { this.redirectUri = redirectUri }

    /**
     * Set a returned [authorization code](https://developer.spotify.com/documentation/general/guides/authorization-guide/)
     */
    public fun authorizationCode(authorizationCode: String?): SpotifyApiBuilder =
        apply { this.authorization.authorizationCode = authorizationCode }

    /**
     * If you only have an access token, the api can be instantiated with it
     */
    public fun tokenString(tokenString: String?): SpotifyApiBuilder =
        apply { this.authorization.tokenString = tokenString }

    /**
     * Set the token to be used with this api instance
     */
    public fun token(token: Token?): SpotifyApiBuilder = apply { this.authorization.token = token }

    /**
     * Enable or disable automatic refresh of the Spotify access token
     */
    public fun automaticRefresh(automaticRefresh: Boolean): SpotifyApiBuilder =
        apply { this.options.automaticRefresh = automaticRefresh }

    /**
     * Set whether to block the current thread and wait until the API can retry the request
     */
    public fun retryWhenRateLimited(retryWhenRateLimited: Boolean): SpotifyApiBuilder =
        apply { this.options.retryWhenRateLimited = retryWhenRateLimited }

    /**
     * Set the maximum time, in milliseconds, before terminating an http request
     */
    public fun requestTimeoutMillis(requestTimeoutMillis: Long?): SpotifyApiBuilder =
        apply { this.options.requestTimeoutMillis = requestTimeoutMillis }

    /**
     * Set whether you want to allow splitting too-large requests into smaller, allowable api requests
     */
    public fun allowBulkRequests(allowBulkRequests: Boolean): SpotifyApiBuilder =
        apply { this.options.allowBulkRequests = allowBulkRequests }

    /**
     * Create a [SpotifyApi] instance with the given [SpotifyApiBuilder] parameters and the type -
     * [AuthorizationType.Client] for client authentication, or otherwise [AuthorizationType.Application]
     */
    public suspend fun build(type: AuthorizationType): GenericSpotifyApi {
        return if (type == AuthorizationType.Client) {
            buildClient()
        } else {
            buildCredentialed()
        }
    }

    /**
     * Create a [SpotifyApi] instance with the given [SpotifyApiBuilder] parameters and the type -
     * [AuthorizationType.Client] for client authentication, or otherwise [AuthorizationType.Application]
     */
    public fun buildRestAction(type: AuthorizationType): SpotifyRestAction<GenericSpotifyApi> = SpotifyRestAction {
        build(type)
    }

    /**
     * Create a new [SpotifyAppApi] that only has access to *public* endpoints and data
     */
    public suspend fun buildPublic(): SpotifyAppApi = buildCredentialed()

    /**
     * Create a new [SpotifyAppApi] that only has access to *public* endpoints and data
     */
    public fun buildPublicRestAction(): SpotifyRestAction<SpotifyAppApi> = SpotifyRestAction { buildPublic() }

    /**
     * Create a new [SpotifyAppApi] that only has access to *public* endpoints and data
     */
    public suspend fun buildCredentialed(): SpotifyAppApi = spotifyAppApi {
        credentials {
            clientId = this@SpotifyApiBuilder.clientId
            clientSecret = this@SpotifyApiBuilder.clientSecret
        }
        authorization(authorization)
        options(options)
    }.build()

    /**
     * Create a new [SpotifyAppApi] that only has access to *public* endpoints and data
     */
    public fun buildCredentialedRestAction(): SpotifyRestAction<SpotifyAppApi> = SpotifyRestAction { buildCredentialed() }

    /**
     * Create a new [SpotifyClientApi] that has access to public endpoints, in addition to endpoints
     * requiring scopes contained in the client authorization request
     */
    public suspend fun buildClient(): SpotifyClientApi = spotifyClientApi {
        credentials {
            clientId = this@SpotifyApiBuilder.clientId
            clientSecret = this@SpotifyApiBuilder.clientSecret
            redirectUri = this@SpotifyApiBuilder.redirectUri
        }
        authorization(authorization)
        options(options)
    }.build()

    /**
     * Create a new [SpotifyClientApi] that has access to public endpoints, in addition to endpoints
     * requiring scopes contained in the client authorization request
     */
    public fun buildClientRestAction(): SpotifyRestAction<SpotifyClientApi> = SpotifyRestAction { buildClient() }
}

/**
 * The type of Spotify authorization used to build an Api instance
 */
public enum class AuthorizationType {
    /**
     * Authorization through explicit affirmative action taken by a client (user) allowing the application to access a/multiple [SpotifyScope]
     *
     * [Spotify application settings page](https://developer.spotify.com/documentation/general/guides/app-settings/)
     */
    Client,

    /**
     * Authorization through application client id and secret, allowing access only to public endpoints and data
     *
     * [Spotify application settings page](https://developer.spotify.com/documentation/general/guides/app-settings/)
     */
    Application;
}

/**
 * Spotify Api builder interface
 *
 * @param T The type of [SpotifyApi] to be built
 * @param B The associated Api builder
Download .txt
gitextract_6wsxvdzq/

├── .github/
│   ├── .github/
│   │   └── FUNDING.yml
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── ci-client.yml
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── README_ANDROID.md
├── TESTING.md
├── build.gradle.kts
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── publish_all.sh
├── settings.gradle.kts
├── src/
│   ├── androidMain/
│   │   ├── AndroidManifest.xml
│   │   ├── kotlin/
│   │   │   └── com/
│   │   │       └── adamratzman/
│   │   │           └── spotify/
│   │   │               ├── auth/
│   │   │               │   ├── SpotifyDefaultCredentialStore.kt
│   │   │               │   ├── implicit/
│   │   │               │   │   ├── AbstractSpotifyAppCompatImplicitLoginActivity.kt
│   │   │               │   │   ├── AbstractSpotifyAppImplicitLoginActivity.kt
│   │   │               │   │   ├── ImplicitAuthUtils.kt
│   │   │               │   │   └── SpotifyImplicitLoginActivity.kt
│   │   │               │   └── pkce/
│   │   │               │       ├── AbstractSpotifyPkceLoginActivity.kt
│   │   │               │       └── PkceAuthUtils.kt
│   │   │               ├── notifications/
│   │   │               │   ├── AbstractSpotifyBroadcastReceiver.kt
│   │   │               │   └── SpotifyBroadcastReceiverUtils.kt
│   │   │               └── utils/
│   │   │                   └── PlatformUtils.kt
│   │   └── res/
│   │       └── layout/
│   │           └── spotify_pkce_auth_layout.xml
│   ├── commonJvmLikeMain/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   ├── javainterop/
│   │                   │   └── SpotifyContinuation.kt
│   │                   └── utils/
│   │                       └── DateTimeUtils.kt
│   ├── commonJvmLikeTest/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   └── CommonImpl.kt
│   ├── commonMain/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify/
│   │           ├── SpotifyApi.kt
│   │           ├── SpotifyApiBuilder.kt
│   │           ├── SpotifyException.kt
│   │           ├── SpotifyRestAction.kt
│   │           ├── SpotifyScope.kt
│   │           ├── annotations/
│   │           │   └── ExperimentalAnnotations.kt
│   │           ├── endpoints/
│   │           │   ├── client/
│   │           │   │   ├── ClientEpisodeApi.kt
│   │           │   │   ├── ClientFollowingApi.kt
│   │           │   │   ├── ClientLibraryApi.kt
│   │           │   │   ├── ClientPersonalizationApi.kt
│   │           │   │   ├── ClientPlayerApi.kt
│   │           │   │   ├── ClientPlaylistApi.kt
│   │           │   │   ├── ClientProfileApi.kt
│   │           │   │   └── ClientShowApi.kt
│   │           │   └── pub/
│   │           │       ├── AlbumApi.kt
│   │           │       ├── ArtistApi.kt
│   │           │       ├── BrowseApi.kt
│   │           │       ├── EpisodeApi.kt
│   │           │       ├── FollowingApi.kt
│   │           │       ├── MarketsApi.kt
│   │           │       ├── PlaylistApi.kt
│   │           │       ├── SearchApi.kt
│   │           │       ├── ShowApi.kt
│   │           │       ├── TrackApi.kt
│   │           │       └── UserApi.kt
│   │           ├── http/
│   │           │   ├── Endpoints.kt
│   │           │   └── HttpRequest.kt
│   │           ├── models/
│   │           │   ├── Albums.kt
│   │           │   ├── Artists.kt
│   │           │   ├── Authentication.kt
│   │           │   ├── Browse.kt
│   │           │   ├── Episode.kt
│   │           │   ├── Library.kt
│   │           │   ├── LocalTracks.kt
│   │           │   ├── Misc.kt
│   │           │   ├── PagingObjects.kt
│   │           │   ├── Playable.kt
│   │           │   ├── Player.kt
│   │           │   ├── Playlist.kt
│   │           │   ├── ResultObjects.kt
│   │           │   ├── Show.kt
│   │           │   ├── SpotifySearchResult.kt
│   │           │   ├── SpotifyUris.kt
│   │           │   ├── Track.kt
│   │           │   ├── Users.kt
│   │           │   └── serialization/
│   │           │       └── SerializationUtils.kt
│   │           └── utils/
│   │               ├── ConcurrentHashMap.kt
│   │               ├── Encoding.kt
│   │               ├── ExternalUrls.kt
│   │               ├── IO.kt
│   │               ├── Language.kt
│   │               ├── Locale.kt
│   │               ├── Market.kt
│   │               ├── Platform.kt
│   │               ├── TimeUnit.kt
│   │               └── Utils.kt
│   ├── commonNonJvmTargetsTest/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify/
│   │           └── CommonImpl.kt
│   ├── commonTest/
│   │   ├── kotlin/
│   │   │   └── com.adamratzman/
│   │   │       └── spotify/
│   │   │           ├── AbstractTest.kt
│   │   │           ├── Common.kt
│   │   │           ├── priv/
│   │   │           │   ├── ClientEpisodeApiTest.kt
│   │   │           │   ├── ClientFollowingApiTest.kt
│   │   │           │   ├── ClientLibraryApiTest.kt
│   │   │           │   ├── ClientPersonalizationApiTest.kt
│   │   │           │   ├── ClientPlayerApiTest.kt
│   │   │           │   ├── ClientPlaylistApiTest.kt
│   │   │           │   └── ClientUserApiTest.kt
│   │   │           ├── pub/
│   │   │           │   ├── BrowseApiTest.kt
│   │   │           │   ├── EpisodeApiTest.kt
│   │   │           │   ├── MarketsApiTest.kt
│   │   │           │   ├── PublicAlbumsApiTest.kt
│   │   │           │   ├── PublicArtistsApiTest.kt
│   │   │           │   ├── PublicFollowingApiTest.kt
│   │   │           │   ├── PublicPlaylistsApiTest.kt
│   │   │           │   ├── PublicTracksApiTest.kt
│   │   │           │   ├── PublicUserApiTest.kt
│   │   │           │   ├── SearchApiTest.kt
│   │   │           │   └── ShowApiTest.kt
│   │   │           └── utilities/
│   │   │               ├── JsonTests.kt
│   │   │               ├── RestTests.kt
│   │   │               ├── UrisTests.kt
│   │   │               └── UtilityTests.kt
│   │   └── resources/
│   │       └── cached_responses.json
│   ├── desktopMain/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   └── utils/
│   │                       └── PlatformUtils.kt
│   ├── iosMain/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify.utils/
│   │           └── PlatformUtils.kt
│   ├── jsMain/
│   │   └── kotlin/
│   │       ├── co.scdn.sdk/
│   │       │   └── SpotifyPlayerJs.kt
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   ├── utils/
│   │                   │   ├── ImplicitGrant.kt
│   │                   │   └── PlatformUtils.kt
│   │                   └── webplayer/
│   │                       └── WebPlaybackSdk.kt
│   ├── jvmMain/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   └── utils/
│   │                       └── PlatformUtils.kt
│   ├── jvmTest/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   └── PkceTest.kt
│   ├── linuxX64Main/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify.utils/
│   │           └── PlatformUtils.kt
│   ├── macosX64Main/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify.utils/
│   │           └── PlatformUtils.kt
│   ├── mingwX64Main/
│   │   └── kotlin/
│   │       └── com.adamratzman.spotify.utils/
│   │           └── PlatformUtils.kt
│   ├── nativeDarwinMain/
│   │   └── kotlin/
│   │       └── com/
│   │           └── adamratzman/
│   │               └── spotify/
│   │                   └── utils/
│   │                       └── PlatformUtils.kt
│   └── tvosMain/
│       └── kotlin/
│           └── com.adamratzman.spotify.utils/
│               └── PlatformUtils.kt
├── webpack.config.d/
│   └── patch.js
└── webpack.config.js
Condensed preview — 133 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,824K chars).
[
  {
    "path": ".github/.github/FUNDING.yml",
    "chars": 731,
    "preview": "# These are supported funding model platforms\n\ngithub: adamint # Replace with up to 4 GitHub Sponsors-enabled usernames "
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 731,
    "preview": "# These are supported funding model platforms\n\ngithub: adamint # Replace with up to 4 GitHub Sponsors-enabled usernames "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 872,
    "preview": "---\r\nname: Bug report\r\nabout: Create a report to help us improve\r\ntitle: ''\r\nlabels: ''\r\nassignees: ''\r\n\r\n---\r\n\r\n**Descr"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 615,
    "preview": "---\r\nname: Feature request\r\nabout: Suggest an idea for this project\r\ntitle: ''\r\nlabels: ''\r\nassignees: ''\r\n\r\n---\r\n\r\n**Is"
  },
  {
    "path": ".github/workflows/ci-client.yml",
    "chars": 1292,
    "preview": "name: CI Client Test Workflow\non:\n  workflow_dispatch:\n    inputs:\n      spotify_test_client_token:\n        description:"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 917,
    "preview": "name: CI Test Workflow\n\non:\n  push:\n    branches: [ main, dev, dev/** ]\n  pull_request:\n    branches: [ main, dev, dev/*"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 3782,
    "preview": "name: Deployment workflow\non:\n  workflow_dispatch:\n    inputs:\n      release_version:\n        description: 'Semantic ver"
  },
  {
    "path": ".gitignore",
    "chars": 6460,
    "preview": "\r\n.DS_STORE\r\n# Created by https://www.toptal.com/developers/gitignore/api/gradle,kotlin,android,intellij+all,node\r\n# Edi"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 471,
    "preview": "# Contributing\r\n\r\nWhen contributing to this repository, feel free to first discuss the change you wish to make via issue"
  },
  {
    "path": "LICENSE",
    "chars": 1090,
    "preview": "MIT License\r\n\r\nCopyright (c) 2017 Adam Ratzman\r\n\r\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "README.md",
    "chars": 27995,
    "preview": "# Kotlin Spotify Web API \nA [Kotlin](https://kotlinlang.org/) implementation of the [Spotify Web API](https://developer."
  },
  {
    "path": "README_ANDROID.md",
    "chars": 14074,
    "preview": "# spotify-web-api-kotlin Android target extended features\n\nPlease also read the Android section of the spotify-web-api-k"
  },
  {
    "path": "TESTING.md",
    "chars": 1869,
    "preview": "# Testing\r\n\r\nWe use the multiplatform kotlin.test framework to run tests.\r\n\r\nYou must create a Spotify application [here"
  },
  {
    "path": "build.gradle.kts",
    "chars": 14728,
    "preview": "@file:Suppress(\"UnstableApiUsage\")\n\nimport com.fasterxml.jackson.databind.json.JsonMapper\nimport org.jetbrains.dokka.gra"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 250,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
  },
  {
    "path": "gradle.properties",
    "chars": 577,
    "preview": "systemProp.org.gradle.internal.publish.checksums.insecure=true\n\norg.gradle.daemon=true\norg.gradle.jvmargs=-Xmx8000m\norg."
  },
  {
    "path": "gradlew",
    "chars": 8669,
    "preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "gradlew.bat",
    "chars": 2868,
    "preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
  },
  {
    "path": "publish_all.sh",
    "chars": 443,
    "preview": "gradle publishMacosX64PublicationToNexusRepository publishLinuxX64PublicationToNexusRepository publishKotlinMultiplatfor"
  },
  {
    "path": "settings.gradle.kts",
    "chars": 1414,
    "preview": "pluginManagement {\n    val kotlinVersion: String by settings\n    val androidBuildToolsVersion: String by settings\n\n    p"
  },
  {
    "path": "src/androidMain/AndroidManifest.xml",
    "chars": 165,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          pa"
  },
  {
    "path": "src/androidMain/kotlin/com/adamratzman/spotify/auth/SpotifyDefaultCredentialStore.kt",
    "chars": 9633,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/androidMain/kotlin/com/adamratzman/spotify/auth/implicit/AbstractSpotifyAppCompatImplicitLoginActivity.kt",
    "chars": 1342,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/androidMain/kotlin/com/adamratzman/spotify/auth/implicit/AbstractSpotifyAppImplicitLoginActivity.kt",
    "chars": 1341,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/androidMain/kotlin/com/adamratzman/spotify/auth/implicit/ImplicitAuthUtils.kt",
    "chars": 2046,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/androidMain/kotlin/com/adamratzman/spotify/auth/implicit/SpotifyImplicitLoginActivity.kt",
    "chars": 5813,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/androidMain/kotlin/com/adamratzman/spotify/auth/pkce/AbstractSpotifyPkceLoginActivity.kt",
    "chars": 8359,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/androidMain/kotlin/com/adamratzman/spotify/auth/pkce/PkceAuthUtils.kt",
    "chars": 968,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/androidMain/kotlin/com/adamratzman/spotify/notifications/AbstractSpotifyBroadcastReceiver.kt",
    "chars": 5728,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/androidMain/kotlin/com/adamratzman/spotify/notifications/SpotifyBroadcastReceiverUtils.kt",
    "chars": 1119,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/androidMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt",
    "chars": 1492,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/androidMain/res/layout/spotify_pkce_auth_layout.xml",
    "chars": 526,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n         "
  },
  {
    "path": "src/commonJvmLikeMain/kotlin/com/adamratzman/spotify/javainterop/SpotifyContinuation.kt",
    "chars": 1184,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:JvmName(\"SpotifyConti"
  },
  {
    "path": "src/commonJvmLikeMain/kotlin/com/adamratzman/spotify/utils/DateTimeUtils.kt",
    "chars": 455,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonJvmLikeTest/kotlin/com/adamratzman/spotify/CommonImpl.kt",
    "chars": 4097,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApi.kt",
    "chars": 30396,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:Suppress(\"LeakingThis"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt",
    "chars": 48936,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/SpotifyException.kt",
    "chars": 3799,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/SpotifyRestAction.kt",
    "chars": 4472,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/SpotifyScope.kt",
    "chars": 4656,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/annotations/ExperimentalAnnotations.kt",
    "chars": 468,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientEpisodeApi.kt",
    "chars": 3004,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt",
    "chars": 14679,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientLibraryApi.kt",
    "chars": 12707,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPersonalizationApi.kt",
    "chars": 5459,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlayerApi.kt",
    "chars": 23387,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlaylistApi.kt",
    "chars": 27658,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientProfileApi.kt",
    "chars": 1502,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientShowApi.kt",
    "chars": 4285,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/AlbumApi.kt",
    "chars": 4492,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/ArtistApi.kt",
    "chars": 6624,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/BrowseApi.kt",
    "chars": 26997,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/EpisodeApi.kt",
    "chars": 4251,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/FollowingApi.kt",
    "chars": 2731,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/MarketsApi.kt",
    "chars": 1035,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/PlaylistApi.kt",
    "chars": 6467,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/SearchApi.kt",
    "chars": 20807,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/ShowApi.kt",
    "chars": 6090,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/TrackApi.kt",
    "chars": 5758,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/UserApi.kt",
    "chars": 1390,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/http/Endpoints.kt",
    "chars": 11692,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/http/HttpRequest.kt",
    "chars": 11518,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Albums.kt",
    "chars": 9733,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Artists.kt",
    "chars": 2662,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Authentication.kt",
    "chars": 2272,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Browse.kt",
    "chars": 2337,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Episode.kt",
    "chars": 10981,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Library.kt",
    "chars": 1330,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/LocalTracks.kt",
    "chars": 4629,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Misc.kt",
    "chars": 720,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/PagingObjects.kt",
    "chars": 23264,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Playable.kt",
    "chars": 2486,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Player.kt",
    "chars": 8199,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Playlist.kt",
    "chars": 7168,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/ResultObjects.kt",
    "chars": 4720,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Show.kt",
    "chars": 5092,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/SpotifySearchResult.kt",
    "chars": 1209,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/SpotifyUris.kt",
    "chars": 18386,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:Suppress(\"EXPERIMENTA"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Track.kt",
    "chars": 24870,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/Users.kt",
    "chars": 5424,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/models/serialization/SerializationUtils.kt",
    "chars": 8438,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/utils/ConcurrentHashMap.kt",
    "chars": 489,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/utils/Encoding.kt",
    "chars": 532,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/utils/ExternalUrls.kt",
    "chars": 1945,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/utils/IO.kt",
    "chars": 1034,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/utils/Language.kt",
    "chars": 26260,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/utils/Locale.kt",
    "chars": 18306,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/utils/Market.kt",
    "chars": 66867,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/utils/Platform.kt",
    "chars": 313,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/utils/TimeUnit.kt",
    "chars": 361,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonMain/kotlin/com.adamratzman.spotify/utils/Utils.kt",
    "chars": 1274,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonNonJvmTargetsTest/kotlin/com.adamratzman.spotify/CommonImpl.kt",
    "chars": 641,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/AbstractTest.kt",
    "chars": 1530,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/Common.kt",
    "chars": 3033,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientEpisodeApiTest.kt",
    "chars": 1635,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientFollowingApiTest.kt",
    "chars": 4120,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientLibraryApiTest.kt",
    "chars": 7770,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientPersonalizationApiTest.kt",
    "chars": 1333,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientPlayerApiTest.kt",
    "chars": 12591,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientPlaylistApiTest.kt",
    "chars": 8757,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientUserApiTest.kt",
    "chars": 775,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/BrowseApiTest.kt",
    "chars": 7785,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/EpisodeApiTest.kt",
    "chars": 1762,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/MarketsApiTest.kt",
    "chars": 747,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicAlbumsApiTest.kt",
    "chars": 2565,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicArtistsApiTest.kt",
    "chars": 3089,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicFollowingApiTest.kt",
    "chars": 1787,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt",
    "chars": 3889,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicTracksApiTest.kt",
    "chars": 2446,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicUserApiTest.kt",
    "chars": 915,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/SearchApiTest.kt",
    "chars": 4393,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/pub/ShowApiTest.kt",
    "chars": 2021,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt",
    "chars": 5051,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/utilities/RestTests.kt",
    "chars": 1510,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/utilities/UrisTests.kt",
    "chars": 11060,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/commonTest/kotlin/com.adamratzman/spotify/utilities/UtilityTests.kt",
    "chars": 5027,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/commonTest/resources/cached_responses.json",
    "chars": 2061505,
    "preview": "{\"ClientEpisodeApiTest.testGetEpisodes\":[\"{\\n    \\\"request\\\": {\\n        \\\"url\\\": \\\"https://api.spotify.com/v1/episodes?"
  },
  {
    "path": "src/desktopMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt",
    "chars": 886,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/iosMain/kotlin/com.adamratzman.spotify.utils/PlatformUtils.kt",
    "chars": 285,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/jsMain/kotlin/co.scdn.sdk/SpotifyPlayerJs.kt",
    "chars": 2157,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage co.scdn.sdk\n\nimport"
  },
  {
    "path": "src/jsMain/kotlin/com/adamratzman/spotify/utils/ImplicitGrant.kt",
    "chars": 1248,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:Suppress(\"unused\")\n\np"
  },
  {
    "path": "src/jsMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt",
    "chars": 1191,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/jsMain/kotlin/com/adamratzman/spotify/webplayer/WebPlaybackSdk.kt",
    "chars": 11389,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:JsQualifier(\"window.S"
  },
  {
    "path": "src/jvmMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt",
    "chars": 700,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/jvmTest/kotlin/com/adamratzman/spotify/PkceTest.kt",
    "chars": 3355,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\n@file:OptIn(ExperimentalCor"
  },
  {
    "path": "src/linuxX64Main/kotlin/com.adamratzman.spotify.utils/PlatformUtils.kt",
    "chars": 285,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/macosX64Main/kotlin/com.adamratzman.spotify.utils/PlatformUtils.kt",
    "chars": 285,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/mingwX64Main/kotlin/com.adamratzman.spotify.utils/PlatformUtils.kt",
    "chars": 285,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/nativeDarwinMain/kotlin/com/adamratzman/spotify/utils/PlatformUtils.kt",
    "chars": 886,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "src/tvosMain/kotlin/com.adamratzman.spotify.utils/PlatformUtils.kt",
    "chars": 285,
    "preview": "/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */\npackage com.adamratzman.spo"
  },
  {
    "path": "webpack.config.d/patch.js",
    "chars": 85,
    "preview": "config.resolve.alias = {\n    \"crypto\": false,\n}\n\noutput: {\n    globalObject: 'this'\n}"
  },
  {
    "path": "webpack.config.js",
    "chars": 69,
    "preview": "module.exports = {\n    output: {\n        globalObject: 'this'\n    }\n}"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the adamint/spotify-web-api-kotlin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 133 files (2.7 MB), approximately 726.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!