Skip to content

Commit

Permalink
Merge pull request #26 from sjdonado/feat/deezer-inverted-search
Browse files Browse the repository at this point in the history
Feat/deezer inverted search
  • Loading branch information
sjdonado authored Jul 28, 2024
2 parents b69634d + b27a64c commit b29c74d
Show file tree
Hide file tree
Showing 13 changed files with 158 additions and 48 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
| Spotify | Yes | Yes | Yes |
| Youtube Music | Yes | No | Yes |
| Apple Music | Yes | No | Yes |
| Deezer | No | Yes | Yes |
| Deezer | Yes | Yes | Yes |
| SoundCloud | No | No | Yes |
| Tidal | No | No | No |

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"dev": "concurrently \"bun run build:dev\" \"bun run --watch www/bin.ts\"",
"build:dev": "vite build --mode=development --watch",
"build": "vite build",
"test": "NODE_ENV=test bun test"
"test": "NODE_ENV=test bun test",
"lint": "eslint --fix",
"tscheck": "tsc --noEmit"
},
"dependencies": {
"@elysiajs/html": "^1.0.2",
Expand Down
5 changes: 4 additions & 1 deletion src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export const YOUTUBE_LINK_REGEX =
export const APPLE_MUSIC_LINK_REGEX =
/^https:\/\/music\.apple\.com\/(?:[a-z]{2}\/)?(?:album|playlist|station|artist|music-video|video-playlist|show)\/([\w-]+)(?:\/([\w-]+))?(?:\?i=(\d+))?(?:\?.*)?$/;

export const ALLOWED_LINKS_REGEX = `${SPOTIFY_LINK_REGEX.source}|${YOUTUBE_LINK_REGEX.source}|${APPLE_MUSIC_LINK_REGEX.source}`;
export const DEEZER_LINK_REGEX =
/^https:\/\/www\.deezer\.com\/(?:[a-z]{2}\/)?(?:track|album|playlist|artist|episode|show)\/(\d+)$/;

export const ALLOWED_LINKS_REGEX = `${SPOTIFY_LINK_REGEX.source}|${YOUTUBE_LINK_REGEX.source}|${APPLE_MUSIC_LINK_REGEX.source}|${DEEZER_LINK_REGEX.source}`;

export const ADAPTERS_QUERY_LIMIT = 1;
export const RESPONSE_COMPARE_MIN_SCORE = 0.4;
Expand Down
5 changes: 3 additions & 2 deletions src/config/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ export enum Adapter {
Spotify = 'spotify',
YouTube = 'youTube',
AppleMusic = 'appleMusic',
Tidal = 'tidal',
SoundCloud = 'soundCloud',
Deezer = 'deezer',
SoundCloud = 'soundCloud',
Tidal = 'tidal',
}

export enum Parser {
Spotify = 'spotify',
YouTube = 'youTube',
AppleMusic = 'appleMusic',
Deezer = 'deezer',
}

export enum MetadataType {
Expand Down
13 changes: 8 additions & 5 deletions src/parsers/appleMusic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MetadataType } from '~/config/enum';
import { MetadataType, Parser } from '~/config/enum';

import { logger } from '~/utils/logger';
import { getCheerioDoc, metaTagContent } from '~/utils/scraper';
Expand All @@ -22,7 +22,7 @@ const APPLE_MUSIC_METADATA_TO_METADATA_TYPE = {
};

export const getAppleMusicMetadata = async (id: string, link: string) => {
const cached = await getCachedSearchMetadata(id);
const cached = await getCachedSearchMetadata(id, Parser.AppleMusic);
if (cached) {
logger.info(`[AppleMusic] (${id}) metadata cache hit`);
return cached;
Expand All @@ -43,16 +43,19 @@ export const getAppleMusicMetadata = async (id: string, link: string) => {
}

const parsedTitle = title?.replace(/on\sApple\sMusic/i, '').trim();
const parsedDescription = description
?.replace(/(Listen to\s|on\sApple\sMusic)/gi, '')
.trim();

const metadata = {
id,
title: parsedTitle,
description,
description: parsedDescription,
type: APPLE_MUSIC_METADATA_TO_METADATA_TYPE[type as AppleMusicMetadataType],
image,
} as SearchMetadata;

await cacheSearchMetadata(id, metadata);
await cacheSearchMetadata(id, Parser.AppleMusic, metadata);

return metadata;
} catch (err) {
Expand All @@ -64,7 +67,7 @@ export const getAppleMusicQueryFromMetadata = (metadata: SearchMetadata) => {
let query = metadata.title;

if (metadata.type === MetadataType.Album) {
query = `${query} album`;
query = metadata.description;
}

if (metadata.type === MetadataType.Playlist) {
Expand Down
83 changes: 83 additions & 0 deletions src/parsers/deezer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { MetadataType, Parser } from '~/config/enum';

import { logger } from '~/utils/logger';
import { getCheerioDoc, metaTagContent } from '~/utils/scraper';

import { SearchMetadata } from '~/services/search';
import { cacheSearchMetadata, getCachedSearchMetadata } from '~/services/cache';
import { fetchMetadata } from '~/services/metadata';

enum DeezerMetadataType {
Song = 'music.song',
Album = 'music.album',
Playlist = 'music.playlist',
Artist = 'music.musician',
}

const DEEZER_METADATA_TO_METADATA_TYPE = {
[DeezerMetadataType.Song]: MetadataType.Song,
[DeezerMetadataType.Album]: MetadataType.Album,
[DeezerMetadataType.Playlist]: MetadataType.Playlist,
[DeezerMetadataType.Artist]: MetadataType.Artist,
};

export const getDeezerMetadata = async (id: string, link: string) => {
const cached = await getCachedSearchMetadata(id, Parser.Deezer);
if (cached) {
logger.info(`[Deezer] (${id}) metadata cache hit`);
return cached;
}

try {
const html = await fetchMetadata(link, {});

const doc = getCheerioDoc(html);

const title = metaTagContent(doc, 'og:title', 'property');
const description = metaTagContent(doc, 'og:description', 'property');
const image = metaTagContent(doc, 'og:image', 'property');
const type = metaTagContent(doc, 'og:type', 'property');
const audio = metaTagContent(doc, 'og:audio', 'property');

if (!title || !type || !image) {
throw new Error('Deezer metadata not found');
}

const parsedTitle = title?.trim();

const metadata = {
id,
title: parsedTitle,
description,
type: DEEZER_METADATA_TO_METADATA_TYPE[type as DeezerMetadataType],
image,
audio,
} as SearchMetadata;

await cacheSearchMetadata(id, Parser.Deezer, metadata);

return metadata;
} catch (err) {
throw new Error(`[${getDeezerMetadata.name}] (${link}) ${err}`);
}
};

export const getDeezerQueryFromMetadata = (metadata: SearchMetadata) => {
let query = metadata.title;

const artists = metadata.description.match(/^([^ -]+(?: [^ -]+)*)/)?.[1];

if (metadata.type === MetadataType.Song) {
query = [query, artists].join(' ');
}

if (metadata.type === MetadataType.Album) {
query = [query, artists].join(' ');
}

if (metadata.type === MetadataType.Playlist) {
query = `${query} playlist`;
}

return query;
};
17 changes: 12 additions & 5 deletions src/parsers/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { ParseError } from 'elysia';

import {
APPLE_MUSIC_LINK_REGEX,
DEEZER_LINK_REGEX,
SPOTIFY_LINK_REGEX,
YOUTUBE_LINK_REGEX,
} from '~/config/constants';
import { Adapter } from '~/config/enum';
import { getSourceFromId } from '~/utils/encoding';
import { Parser } from '~/config/enum';

import { getSourceFromId } from '~/utils/encoding';
import { logger } from '~/utils/logger';

export type SearchParser = {
Expand Down Expand Up @@ -37,20 +38,26 @@ export const getSearchParser = (link?: string, searchId?: string) => {
const spotifyId = source.match(SPOTIFY_LINK_REGEX)?.[3];
if (spotifyId) {
id = spotifyId;
type = Adapter.Spotify;
type = Parser.Spotify;
}

const youtubeId = source.match(YOUTUBE_LINK_REGEX)?.[1];
if (youtubeId) {
id = youtubeId;
type = Adapter.YouTube;
type = Parser.YouTube;
}

const match = source.match(APPLE_MUSIC_LINK_REGEX);
const appleMusicId = match ? match[3] || match[2] || match[1] : null;
if (appleMusicId) {
id = appleMusicId;
type = Adapter.AppleMusic;
type = Parser.AppleMusic;
}

const deezerId = source.match(DEEZER_LINK_REGEX)?.[1];
if (deezerId) {
id = deezerId;
type = Parser.Deezer;
}

if (!id || !type) {
Expand Down
6 changes: 3 additions & 3 deletions src/parsers/spotify.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MetadataType } from '~/config/enum';
import { MetadataType, Parser } from '~/config/enum';

import { logger } from '~/utils/logger';
import { getCheerioDoc, metaTagContent } from '~/utils/scraper';
Expand Down Expand Up @@ -32,7 +32,7 @@ const SPOTIFY_METADATA_TO_METADATA_TYPE = {
};

export const getSpotifyMetadata = async (id: string, link: string) => {
const cached = await getCachedSearchMetadata(id);
const cached = await getCachedSearchMetadata(id, Parser.Spotify);
if (cached) {
logger.info(`[Spotify] (${id}) metadata cache hit`);
return cached;
Expand Down Expand Up @@ -83,7 +83,7 @@ export const getSpotifyMetadata = async (id: string, link: string) => {
audio,
} as SearchMetadata;

await cacheSearchMetadata(id, metadata);
await cacheSearchMetadata(id, Parser.Spotify, metadata);

return metadata;
} catch (err) {
Expand Down
7 changes: 3 additions & 4 deletions src/parsers/youtube.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MetadataType } from '~/config/enum';
import { MetadataType, Parser } from '~/config/enum';

import { logger } from '~/utils/logger';
import { getCheerioDoc, metaTagContent } from '~/utils/scraper';
Expand All @@ -24,9 +24,8 @@ const YOUTUBE_METADATA_TO_METADATA_TYPE = {
[YouTubeMetadataType.Podcast]: MetadataType.Podcast,
[YouTubeMetadataType.Show]: MetadataType.Show,
};

export const getYouTubeMetadata = async (id: string, link: string) => {
const cached = await getCachedSearchMetadata(id);
const cached = await getCachedSearchMetadata(id, Parser.YouTube);
if (cached) {
logger.info(`[YouTube] (${id}) metadata cache hit`);
return cached;
Expand Down Expand Up @@ -59,7 +58,7 @@ export const getYouTubeMetadata = async (id: string, link: string) => {
image,
} as SearchMetadata;

await cacheSearchMetadata(id, metadata);
await cacheSearchMetadata(id, Parser.YouTube, metadata);

return metadata;
} catch (err) {
Expand Down
15 changes: 10 additions & 5 deletions src/services/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { caching } from 'cache-manager';
import bunSqliteStore from 'cache-manager-bun-sqlite3';

import { ENV } from '~/config/env';
import type { Parser } from '~/config/enum';

import { SearchMetadata, SearchResultLink } from './search';

export const cacheStore = await caching(bunSqliteStore, {
Expand All @@ -23,20 +25,23 @@ export const getCachedSearchResultLink = async (url: URL) => {
return data;
};

export const cacheSearchMetadata = async (id: string, searchMetadata: SearchMetadata) => {
await cacheStore.set(`metadata:${id}`, searchMetadata);
export const cacheSearchMetadata = async (
id: string,
parser: Parser,
searchMetadata: SearchMetadata
) => {
await cacheStore.set(`metadata:${parser}:${id}`, searchMetadata);
};

export const getCachedSearchMetadata = async (id: string) => {
const data = (await cacheStore.get(`metadata:${id}`)) as SearchMetadata;
export const getCachedSearchMetadata = async (id: string, parser: Parser) => {
const data = (await cacheStore.get(`metadata:${parser}:${id}`)) as SearchMetadata;

return data;
};

export const cacheSpotifyAccessToken = async (accessToken: string, expTime: number) => {
await cacheStore.set('spotify:accessToken', accessToken, expTime);
};

export const getCachedSpotifyAccessToken = async (): Promise<string | undefined> => {
return cacheStore.get('spotify:accessToken');
};
Expand Down
37 changes: 22 additions & 15 deletions src/services/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getAppleMusicMetadata,
getAppleMusicQueryFromMetadata,
} from '~/parsers/appleMusic';
import { getDeezerMetadata, getDeezerQueryFromMetadata } from '~/parsers/deezer';

export type SearchMetadata = {
title: string;
Expand Down Expand Up @@ -68,23 +69,27 @@ export const search = async ({

let metadata, query;

if (searchParser.type === Parser.Spotify) {
metadata = await getSpotifyMetadata(searchParser.id, searchParser.source);
query = getSpotifyQueryFromMetadata(metadata);
}

if (searchParser.type === Parser.YouTube) {
metadata = await getYouTubeMetadata(searchParser.id, searchParser.source);
query = getYouTubeQueryFromMetadata(metadata);
}

if (searchParser.type === Parser.AppleMusic) {
metadata = await getAppleMusicMetadata(searchParser.id, searchParser.source);
query = getAppleMusicQueryFromMetadata(metadata);
switch (searchParser.type) {
case Parser.Spotify:
metadata = await getSpotifyMetadata(searchParser.id, searchParser.source);
query = getSpotifyQueryFromMetadata(metadata);
break;
case Parser.YouTube:
metadata = await getYouTubeMetadata(searchParser.id, searchParser.source);
query = getYouTubeQueryFromMetadata(metadata);
break;
case Parser.AppleMusic:
metadata = await getAppleMusicMetadata(searchParser.id, searchParser.source);
query = getAppleMusicQueryFromMetadata(metadata);
break;
case Parser.Deezer:
metadata = await getDeezerMetadata(searchParser.id, searchParser.source);
query = getDeezerQueryFromMetadata(metadata);
break;
}

if (!metadata || !query) {
throw new Error('Adapter not implemented yet');
throw new Error('Parser not implemented yet');
}

logger.info(
Expand Down Expand Up @@ -131,7 +136,9 @@ export const search = async ({
searchParser.type !== Parser.AppleMusic && searchAdapters.includes(Adapter.AppleMusic)
? getAppleMusicLink(query, metadata)
: null,
searchAdapters.includes(Adapter.Deezer) ? getDeezerLink(query, metadata) : null,
searchParser.type !== Parser.Deezer && searchAdapters.includes(Adapter.Deezer)
? getDeezerLink(query, metadata)
: null,
searchAdapters.includes(Adapter.SoundCloud)
? getSoundCloudLink(query, metadata)
: null,
Expand Down
6 changes: 3 additions & 3 deletions src/views/pages/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export default function Home({
<div class="flex h-svh flex-col gap-2 p-2">
<LoadingIndicator />
<main class="flex flex-1 flex-col items-center justify-start">
<div class="my-8 flex flex-col gap-4 p-2 text-center sm:my-12">
<div class="mb-4 mt-8 flex flex-col gap-4 p-2 text-center sm:mt-12">
<h1 class="text-4xl uppercase md:text-5xl lg:text-6xl">I Don't Have Spotify</h1>
<p class="text-justify text-sm text-zinc-400 lg:text-base">
Paste a Spotify, YouTube, or Apple Music link to enjoy music across different
platforms.
Paste a link from Spotify, YouTube, Apple Music, or Deezer to enjoy your music
across multiple platforms.
</p>
</div>
<div
Expand Down
Loading

0 comments on commit b29c74d

Please sign in to comment.