-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This merges Catalogue, Playlist, Search and User into one module instead of four.
- Loading branch information
Showing
20 changed files
with
20,211 additions
and
220 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# Changelog | ||
|
||
All notable changes to this project will be documented in this file. | ||
|
||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), | ||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
||
|
||
## [0.1.0] - 2024-xx-xx | ||
|
||
### Changed | ||
|
||
- Initial release of joined API types and `fetch` wrapper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# TIDAL API v2 Beta | ||
|
||
A thin wrapper around the API domains described at: https://developer.tidal.com/apiref (which is again built on the JSON API spec: https://jsonapi.org/format/) | ||
|
||
The module provides Typescript types and a `fetch` based function for getting data, using: https://openapi-ts.pages.dev/ | ||
|
||
## Usage | ||
|
||
One function is exposed that can be used for creating a function that can then do network calls: `createAPIClient`. Also the API types are exposed and can be used directly. | ||
|
||
### Example | ||
See the `examples/` folder for some ways it can be used. | ||
|
||
To run it do: `pnpm dev` | ||
|
||
## Development | ||
|
||
Run `pnpm generateTypes` to regenerate the types from the API specs. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
/* eslint-disable no-console */ | ||
import { credentialsProvider, init as initAuth } from '@tidal-music/auth'; | ||
|
||
import { createAPIClient } from '../dist'; | ||
|
||
/** | ||
* Runs the example with a client ID and client secret (from https://developer.tidal.com). | ||
* | ||
* @param {string} clientId The client ID. | ||
* @param {string} clientSecret The client secret. | ||
* @param {string} searchTerm The search term. | ||
*/ | ||
async function runExample(clientId, clientSecret, searchTerm) { | ||
await initAuth({ | ||
clientId, | ||
clientSecret, | ||
credentialsStorageKey: 'clientCredentials', | ||
}); | ||
|
||
const apiClient = createAPIClient(credentialsProvider); | ||
const results = document.getElementById('results'); | ||
results.innerHTML = ''; | ||
|
||
/** | ||
* Retrieves an album by its ID. | ||
* | ||
* @param {string} id The ID of the album. | ||
*/ | ||
async function getAlbum(id) { | ||
const { data, error } = await apiClient.GET('/albums/{id}', { | ||
params: { | ||
path: { id }, | ||
query: { countryCode: 'NO' }, | ||
}, | ||
}); | ||
|
||
if (error) { | ||
error.errors.forEach( | ||
err => (results.innerHTML += `<li>${err.detail}</li>`), | ||
); | ||
} else { | ||
for (const [key, value] of Object.entries(data.data.attributes)) { | ||
results.innerHTML += `<li><b>${key}:</b>${JSON.stringify(value)}</li>`; | ||
} | ||
} | ||
} | ||
// Example of an API request | ||
await getAlbum('75413011'); | ||
|
||
/** | ||
* Retrieves an album with the tracks and other relationships. | ||
* | ||
* @param {string} albumId The ID of the album. | ||
*/ | ||
async function getAlbumWithTracks(albumId) { | ||
const { data, error } = await apiClient.GET('/albums/{albumId}', { | ||
params: { | ||
path: { albumId }, | ||
query: { countryCode: 'NO', include: 'items,artists,providers' }, | ||
}, | ||
}); | ||
|
||
if (error) { | ||
console.error(error); | ||
} else { | ||
console.log(data); | ||
console.log(JSON.stringify(data)); | ||
} | ||
} | ||
// Example of another API request | ||
await getAlbumWithTracks('75413011'); | ||
|
||
/** | ||
* Retrieves an artist by its ID. | ||
* | ||
* @param {string} id The ID of the artist. | ||
*/ | ||
async function getArtist(id) { | ||
const { data, error } = await apiClient.GET('/artists/{id}', { | ||
params: { | ||
path: { id }, | ||
query: { countryCode: 'NO' }, | ||
}, | ||
}); | ||
|
||
if (error) { | ||
console.error(error); | ||
} else { | ||
console.log(data.data.attributes.name); | ||
} | ||
} | ||
// Example failing API request | ||
await getArtist('1'); | ||
|
||
/** | ||
* Retrieves a playlist by its ID. | ||
* | ||
* @param {string} id The ID of the playlist. | ||
*/ | ||
async function getPlaylist(id) { | ||
const { data, error } = await apiClient.GET('/playlists/{id}', { | ||
params: { | ||
path: { id }, | ||
query: { countryCode: 'NO' }, | ||
}, | ||
}); | ||
|
||
if (error) { | ||
error.errors.forEach( | ||
err => (results.innerHTML += `<li>${err.detail}</li>`), | ||
); | ||
} else { | ||
for (const [key, value] of Object.entries(data.data.attributes)) { | ||
results.innerHTML += `<li><b>${key}:</b>${JSON.stringify(value)}</li>`; | ||
} | ||
} | ||
} | ||
// Example of a playlist API request | ||
await getPlaylist('bd878bbc-c0a1-4b81-9a2a-a16dc9926300'); | ||
|
||
/** | ||
* Do a search for a term | ||
* | ||
* @param {string} query The search term. | ||
*/ | ||
async function doSearch(query) { | ||
const { data, error } = await apiClient.GET('/searchresults/{query}', { | ||
params: { | ||
path: { query }, | ||
query: { countryCode: 'NO', include: 'topHits' }, | ||
}, | ||
}); | ||
|
||
if (error) { | ||
error.errors.forEach( | ||
err => (results.innerHTML += `<li>${err.detail}</li>`), | ||
); | ||
} else { | ||
data.data.relationships.topHits.data.forEach(hit => { | ||
const item = data.included.find(i => i.id === hit.id); | ||
const text = item.attributes.title || item.attributes.name; | ||
results.innerHTML += `<li><b>${hit.type}:</b>${text}</li>`; | ||
}); | ||
} | ||
} | ||
// Example of a search API request | ||
await doSearch(searchTerm); | ||
|
||
/** | ||
* Retrieves User Public Profile by their ID. | ||
* | ||
* @param {string} id The ID of the user. | ||
*/ | ||
async function getUserPublicProfile(id) { | ||
const { data, error } = await apiClient.GET('/userPublicProfiles/{id}', { | ||
params: { | ||
path: { id }, | ||
query: { countryCode: 'NO' }, | ||
}, | ||
}); | ||
|
||
if (error) { | ||
error.errors.forEach( | ||
err => (results.innerHTML += `<li>${err.detail}</li>`), | ||
); | ||
} else { | ||
for (const [key, value] of Object.entries(data.data)) { | ||
results.innerHTML += `<li><b>${key}:</b>${JSON.stringify(value)}</li>`; | ||
} | ||
} | ||
} | ||
// Example of a user API request | ||
await getUserPublicProfile('12345'); | ||
} | ||
|
||
const authenticateHandler = async (event, form) => { | ||
event.preventDefault(); | ||
|
||
if (!form) { | ||
return; | ||
} | ||
|
||
const formData = new FormData(form); | ||
const clientId = formData.get('clientId'); | ||
const clientSecret = formData.get('clientSecret'); | ||
const searchTerm = formData.get('searchTerm'); | ||
|
||
await runExample(clientId, clientSecret, searchTerm); | ||
}; | ||
|
||
window.addEventListener('load', () => { | ||
const form = document.getElementById('clientCredentialsForm'); | ||
|
||
form?.addEventListener('submit', event => { | ||
authenticateHandler(event, form).catch(error => console.error(error)); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<html> | ||
<head> | ||
<style> | ||
label { | ||
margin-bottom: 5px; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<form id="clientCredentialsForm"> | ||
<label> | ||
<span>Client ID</span> | ||
<input type="text" name="clientId" /> | ||
</label> | ||
<label> | ||
<span>Client Secret</span> | ||
<input type="text" name="clientSecret" /> | ||
</label> | ||
<label> | ||
<span>Search term</span> | ||
<input type="search" name="searchTerm" /> | ||
</label> | ||
<button type="submit">Authenticate and Search</button> | ||
</form> | ||
|
||
<ul id="results"></ul> | ||
|
||
<script type="module" src="./examples/example.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
{ | ||
"name": "@tidal-music/api", | ||
"version": "0.1.0", | ||
"type": "module", | ||
"files": [ | ||
"dist" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "ssh://[email protected]:tidal-music/tidal-sdk-web.git" | ||
}, | ||
"license": "Apache-2.0", | ||
"module": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"exports": { | ||
"default": "./dist/index.js", | ||
"import": "./dist/index.js" | ||
}, | ||
"scripts": { | ||
"prepare": "vite build", | ||
"build": "vite build", | ||
"build:dev": "vite build -m development", | ||
"clean": "rm -rf coverage dist .eslintcache", | ||
"dev": "vite --debug --cors -c=./vite.config.ts", | ||
"lint": "eslint . --cache --cache-strategy content", | ||
"lint:ci": "eslint . --quiet", | ||
"lint:fix": "pnpm run lint --fix", | ||
"preview": "vite preview", | ||
"generateTypes": "openapi-typescript", | ||
"test": "vitest", | ||
"test:coverage": "pnpm run test --coverage", | ||
"test:ui": "pnpm run test:coverage --ui", | ||
"typecheck": "tsc" | ||
}, | ||
"dependencies": { | ||
"@tidal-music/api": "workspace:*", | ||
"openapi-fetch": "0.12.0" | ||
}, | ||
"devDependencies": { | ||
"@tidal-music/auth": "workspace:^", | ||
"@tidal-music/common": "workspace:^", | ||
"@vitest/coverage-v8": "2.0.4", | ||
"@vitest/ui": "2.0.4", | ||
"openapi-typescript": "7.4.0", | ||
"typescript": "5.6.2", | ||
"vite": "5.4.6", | ||
"vite-plugin-dts": "4.2.1", | ||
"vitest": "2.0.4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
apis: | ||
catalogue: | ||
root: https://developer.tidal.com/apiref/api-specifications/api-public-catalogue-jsonapi/tidal-catalog-v2-openapi-3.0.json | ||
x-openapi-ts: | ||
output: ./src/catalogueAPI.generated.ts | ||
playlist: | ||
root: https://developer.tidal.com/apiref/api-specifications/api-public-user-content/tidal-user-content-openapi-3.0.json | ||
x-openapi-ts: | ||
output: ./src/playlistAPI.generated.ts | ||
search: | ||
root: https://developer.tidal.com/apiref/api-specifications/api-public-search-jsonapi/tidal-search-v2-openapi-3.0.json | ||
x-openapi-ts: | ||
output: ./src/searchAPI.generated.ts | ||
user: | ||
root: https://developer.tidal.com/apiref/api-specifications/api-public-user-jsonapi/tidal-user-v2-openapi-3.0.json | ||
x-openapi-ts: | ||
output: ./src/userAPI.generated.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { createAPIClient } from './api'; | ||
|
||
describe('createAPIClient', () => { | ||
it('creates a TIDAL API client', () => { | ||
const client = createAPIClient({ | ||
bus: vi.fn(), | ||
getCredentials: vi.fn(), | ||
}); | ||
expect(client).toBeDefined(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { CredentialsProvider } from '@tidal-music/common'; | ||
import createClient, { type Middleware } from 'openapi-fetch'; | ||
|
||
import type { paths as cataloguePaths } from './catalogueAPI.generated'; | ||
import type { paths as playlistPaths } from './playlistAPI.generated'; | ||
import type { paths as searchPaths } from './searchAPI.generated'; | ||
import type { paths as userPaths } from './userAPI.generated'; | ||
|
||
/** | ||
* Create a Catalogue API client with the provided credentials. | ||
* | ||
* @param credentialsProvider The credentials provider, from Auth module. | ||
*/ | ||
export function createAPIClient(credentialsProvider: CredentialsProvider) { | ||
const authMiddleware: Middleware = { | ||
async onRequest({ request }) { | ||
const credentials = await credentialsProvider.getCredentials(); | ||
|
||
// Add Authorization header to every request | ||
request.headers.set('Authorization', `Bearer ${credentials.token}`); | ||
return request; | ||
}, | ||
}; | ||
|
||
type AllPaths = cataloguePaths & playlistPaths & searchPaths & userPaths; | ||
|
||
const apiClient = createClient<AllPaths>({ | ||
baseUrl: 'https://openapi.tidal.com/v2/', | ||
}); | ||
apiClient.use(authMiddleware); | ||
|
||
return apiClient; | ||
} |
Oops, something went wrong.