Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

37 provide api support #43

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions .env.example

This file was deleted.

2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ SOUNDCLOUD_BASE_URL=https://soundcloud.com

URL_SHORTENER_API_URL=http://localhost:4000/api/links
URL_SHORTENER_API_KEY=url_shortener_api_key

IDHS_API_KEY_BETA=idhs_api_key
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ Adapters represent the streaming services supported by the Web App and the Rayca
<img width="1200" alt="image" src="https://github.com/user-attachments/assets/ae6250f5-d1ed-41f2-ae21-8a2b2599a450" />
</div>

# Apple Shortcuts (beta)
- [IDHS Open In Spotify](https://www.icloud.com/shortcuts/2757d4e10fad4cc182225953c8fdf80e)
- [IDHS Open In Tidal](https://www.icloud.com/shortcuts/63716e2abdcd4cc28fb67abf72e994f9)
- [IDHS Open In Youtube Music](https://www.icloud.com/shortcuts/de21be4d0878440f85bbf10bd6ee049a)
- [IDHS Open In Apple Music](https://www.icloud.com/shortcuts/23edcdae7dab452a9adc926435eafbdc)
- [IDHS Open In Deezer](https://www.icloud.com/shortcuts/f760b0cc6b6b494a88d31c31009966f3)
- [IDHS Open In SoundCloud](https://www.icloud.com/shortcuts/972d8ac3b30b4184a6cd63abb8c5cd9b)

## Raycast Extension

![Uptime Badge](https://uptime.sjdonado.com/api/badge/3/uptime/24?labelPrefix=API%20&labelSuffix=h) ![Uptime Badge](https://uptime.sjdonado.com/api/badge/3/ping/24?labelPrefix=API%20)
Expand All @@ -32,7 +40,14 @@ Adapters represent the streaming services supported by the Web App and the Rayca

## Local Setup (Web App)

The list of environment variables is available in `.env.example`. To complete the values for `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` refer to https://developer.spotify.com/documentation/web-api, other variables related to cookies can be extracted from your browser.
The list of environment variables is available in `.env.test`. To complete the values for the following variables:

- `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET`, refer to [Spotify Web API Documentation](https://developer.spotify.com/documentation/web-api).
- `TIDAL_CLIENT_ID` and `TIDAL_CLIENT_SECRET`, refer to [TIDAL Developer Portal](https://developer.tidal.com/).
- `YOUTUBE_API_KEY`, refer to [Google Developers Console](https://console.developers.google.com/).
- `URL_SHORTENER_API_KEY`, refer to [Bit](https://github.com/sjdonado/bit)

Ensure that the values are correctly added to your `.env` file to configure the API keys properly.

To get the app up:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "idonthavespotify",
"version": "1.5.0",
"version": "1.6.0-beta",
"scripts": {
"dev": "concurrently \"bun run build:dev\" \"bun run --watch www/bin.ts\"",
"build:dev": "vite build --mode=development --watch",
Expand Down
3 changes: 2 additions & 1 deletion src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const ENV = {
baseUrl: Bun.env.SOUNDCLOUD_BASE_URL!,
},
},
utils: {
services: {
urlShortener: {
apiUrl: Bun.env.URL_SHORTENER_API_URL!,
apiKey: Bun.env.URL_SHORTENER_API_KEY!,
Expand All @@ -40,6 +40,7 @@ export const ENV = {
app: {
url: Bun.env.APP_URL!,
version: version,
apiKeyBeta: Bun.env.IDHS_API_KEY_BETA!,
},
cache: {
databasePath: Bun.env.DATABASE_PATH ?? ':memory:',
Expand Down
23 changes: 19 additions & 4 deletions src/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Elysia } from 'elysia';
import { Adapter } from '~/config/enum';
import { search } from '~/services/search';
import { logger } from '~/utils/logger';
import { apiVersionValidator, searchPayloadValidator } from '~/validations/search';
import { apiV2Validator, legacyApiV1Validator } from '~/validations/search';

export const apiRouter = new Elysia().group('/api', app =>
app
Expand All @@ -15,14 +15,29 @@ export const apiRouter = new Elysia().group('/api', app =>
};
})
.post(
'/search',
'/search', // TODO: remove after new Raycast version is released
async ({ body: { link, adapters } }) => {
const searchResult = await search({ link, adapters: adapters as Adapter[] });
return searchResult;
},
{
body: searchPayloadValidator,
query: apiVersionValidator,
query: legacyApiV1Validator.query,
body: legacyApiV1Validator.body,
}
)
.get(
'/search',
async ({ query }) => {
const searchResult = await search({
link: query.link,
adapters: query._adapters as Adapter[],
headless: query.headless,
});
return searchResult;
},
{
query: apiV2Validator.query,
transform: apiV2Validator.transform,
}
)
);
6 changes: 3 additions & 3 deletions src/routes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Elysia, InternalServerError } from 'elysia';

import { search } from '~/services/search';
import { logger } from '~/utils/logger';
import { searchPayloadValidator, searchQueryValidator } from '~/validations/search';
import { legacyApiV1Validator, webValidator } from '~/validations/search';
import ErrorMessage from '~/views/components/error-message';
import SearchCard from '~/views/components/search-card';
import MainLayout from '~/views/layouts/main';
Expand Down Expand Up @@ -61,7 +61,7 @@ export const pageRouter = new Elysia()
}
},
{
query: searchQueryValidator,
query: webValidator.query,
}
)
.post(
Expand All @@ -71,6 +71,6 @@ export const pageRouter = new Elysia()
return <SearchCard searchResult={searchResult} />;
},
{
body: searchPayloadValidator,
body: legacyApiV1Validator.body,
}
);
74 changes: 48 additions & 26 deletions src/services/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,15 @@ export const search = async ({
link,
searchId,
adapters,
headless,
}: {
link?: string;
searchId?: string;
adapters?: Adapter[];
headless?: boolean;
}) => {
const searchParser = getSearchParser(link, searchId);

const searchAdapters = adapters ?? [
Adapter.Spotify,
Adapter.YouTube,
Expand All @@ -70,7 +74,7 @@ export const search = async ({
Adapter.Tidal,
];

const searchParser = getSearchParser(link, searchId);
logger.info(`[search] (searchAdapters) ${searchAdapters}`);

const metadataFetchersMap = {
[Parser.Spotify]: getSpotifyMetadata,
Expand Down Expand Up @@ -106,7 +110,8 @@ export const search = async ({
throw new InternalServerError('Parser not implemented yet');
}

let metadata = await metadataFetcher(searchParser.id, searchParser.source);
// Even if headless, we need initial metadata and query for link extraction
const metadata = await metadataFetcher(searchParser.id, searchParser.source);
const query = queryExtractor(metadata);
const parserType = searchParser.type as StreamingServiceType;

Expand All @@ -127,7 +132,12 @@ export const search = async ({
searchAdapters.length === 1 &&
searchParser.type === (searchAdapters[0] as StreamingServiceType)
) {
logger.info(`[${search.name}] early return - adapter is equal to parser type`);
logger.info(`[${search.name}] (early return) adapter is equal to parser type`);

// If headless, return just the link as a string, else return the full object
if (headless) {
return link;
}

return {
id,
Expand All @@ -143,12 +153,14 @@ export const search = async ({
}

const links: SearchResultLink[] = [];
const existingAdapters = new Set(links.map(link => link.type));
const existingAdapters = new Set<Adapter>();

let tidalLink: SearchResultLink | null = linkSearchResult;
if (parserType !== Adapter.Tidal) {
if (searchAdapters.includes(Adapter.Tidal) && parserType !== Adapter.Tidal) {
tidalLink = await getTidalLink(query, metadata);
existingAdapters.add(Adapter.Tidal);
if (tidalLink) {
existingAdapters.add(Adapter.Tidal);
}
}

if (tidalLink) {
Expand All @@ -171,7 +183,12 @@ export const search = async ({
if (fromTidalULink) {
for (const adapterKey in fromTidalULink) {
const adapter = adapterKey as Adapter;
if (parserType !== adapter && fromTidalULink[adapter]) {
// Only add the adapter if it's requested and not the parser type
if (
searchAdapters.includes(adapter) &&
parserType !== adapter &&
fromTidalULink[adapter]
) {
links.push(fromTidalULink[adapter]);
existingAdapters.add(adapter);
}
Expand Down Expand Up @@ -200,9 +217,23 @@ export const search = async ({
.filter(Boolean)
);

// Fetch metadata audio from spotify and universal link from bit
const parsedLinks = links
.filter(link => searchAdapters.includes(link.type))
.sort((a, b) => {
// Prioritize verified links
if (a.isVerified && !b.isVerified) return -1;
if (!a.isVerified && b.isVerified) return 1;
return a.type.localeCompare(b.type);
});

// If headless is true, skip updatedMetadata and universal link shortening
if (headless) {
return parsedLinks.map(link => link.url);
}

// Fetch updated metadata (for audio) from spotify if not parser type
const spotifyLink = links.find(link => link.type === Adapter.Spotify);
const [updatedMetadata, shortLink] = await Promise.all([
const [parsedMetadata, shortLink] = await Promise.all([
parserType !== Adapter.Spotify && spotifyLink
? (async () => {
const spotifySearchParser = getSearchParser(spotifyLink.url);
Expand All @@ -212,28 +243,19 @@ export const search = async ({
shortenLink(universalLink),
]);

metadata = updatedMetadata;
links.sort((a, b) => {
// Prioritize verified links
if (a.isVerified && !b.isVerified) return -1;
if (!a.isVerified && b.isVerified) return 1;

return a.type.localeCompare(b.type);
});

logger.info(`[${search.name}] (results) ${links.map(link => link?.url)}`);

const searchResult: SearchResult = {
id,
type: metadata.type,
title: metadata.title,
description: metadata.description,
image: metadata.image,
audio: metadata.audio,
type: parsedMetadata.type,
title: parsedMetadata.title,
description: parsedMetadata.description,
image: parsedMetadata.image,
audio: parsedMetadata.audio,
source: searchParser.source,
universalLink: shortLink,
links,
links: parsedLinks,
};

logger.info(`[${search.name}] (results) ${searchResult.links.map(link => link?.url)}`);

return searchResult;
};
4 changes: 2 additions & 2 deletions src/utils/url-shortener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ export async function shortenLink(link: string) {

try {
const response = await HttpClient.post<ApiResponse>(
ENV.utils.urlShortener.apiUrl,
ENV.services.urlShortener.apiUrl,
{
url: link,
},
{
headers: {
'Content-Type': 'application/json',
'X-Api-Key': ENV.utils.urlShortener.apiKey,
'X-Api-Key': ENV.services.urlShortener.apiKey,
},
}
);
Expand Down
Loading
Loading