Skip to content

Commit

Permalink
Merge pull request #37 from toiki-org/feat/albums-playlists
Browse files Browse the repository at this point in the history
feat: support album conversion
  • Loading branch information
LucasVinicius314 authored Feb 23, 2024
2 parents 9a1571e + 46961ed commit fb1d97a
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 38 deletions.
76 changes: 59 additions & 17 deletions bot/src/utils/isYoutubeOrSpotify.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,67 @@
export const youtubeRegexes = [/.*(youtu\.be|youtube.com|music.youtube.com)\/watch\?v=((\w|-)+)?(?=(\&|$))/, /.*(youtu\.be|youtube.com)\/((\w|-)+)?(?=(\?|$))/]
export const spotifyRegex = /.*open.spotify.com\/track\/(\w+)?(?=\?|$)/
export interface Match {
type: 'spotify' | 'youtube'
id: string
kind: 'track' | 'album'
}

interface Match {
type: 'spotify' | 'youtube';
id: string;
interface Matcher {
regex: RegExp[]
idIndex: number
match: Omit<Match, 'id'>
}

export const isYoutubeOrSpotify = (url: string): Match | null => {
const spotifyMatch = url.match(spotifyRegex)
if (spotifyMatch != null && spotifyMatch.length > 1) {
return {
type: 'spotify',
id: spotifyMatch[1]
}
}
let youtubeMatch = url.match(youtubeRegexes[0]) ?? url.match(youtubeRegexes[1])
if (youtubeMatch != null && youtubeMatch.length > 1) {
return {
const matches: Matcher[] = [
{
regex: [
/.*(youtu\.be|youtube.com|music.youtube.com)\/watch\?v=((\w|-)+)?(?=(\&|$))/,
/.*(youtu\.be|youtube.com)\/((?!playlist)(\w|-)+)?(?=(\?|$))/,
],
idIndex: 2,
match: {
type: 'youtube',
kind: 'track',
},
},
{
regex: [
/.*(youtube.com|music.youtube.com)\/playlist\?list=((\w|-)+)?(?=(\&|$))/,
],
idIndex: 2,
match: {
type: 'youtube',
id: youtubeMatch[2]
kind: 'album',
},
},
{
regex: [/.*open.spotify.com\/track\/(\w+)?(?=\?|$)/],
idIndex: 1,
match: {
type: 'spotify',
kind: 'track',
},
},
{
regex: [/.*open.spotify.com\/album\/(\w+)?(?=\?|$)/],
idIndex: 1,
match: {
type: 'spotify',
kind: 'album',
},
},
]

export const isYoutubeOrSpotify = (url: string): Match | null => {
for (const matcher of matches) {
const match = matcher.regex
.map((regex) => url.match(regex))
.find((v) => v != null)
if (!!match && match.length > 1) {
return {
...matcher.match,
id: match[matcher.idIndex],
}
}
}

return null
}
14 changes: 12 additions & 2 deletions server/src/controllers/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,20 @@ export default class ConvertController extends BaseController {

let result: string

const id = matchResult.id

if (matchResult.type === 'spotify') {
result = await this.conversionService.convertSpotifyUrl(matchResult.id)
if (matchResult.kind === 'album') {
result = await this.conversionService.convertSpotifyAlbum(id)
} else {
result = await this.conversionService.convertSpotifyTrack(id)
}
} else {
result = await this.conversionService.convertYoutubeUrl(matchResult.id)
if (matchResult.kind === 'album') {
result = await this.conversionService.convertYoutubeAlbum(id)
} else {
result = await this.conversionService.convertYoutubeTrack(id)
}
}
JSONResponse.success(res, {
url: result,
Expand Down
2 changes: 2 additions & 0 deletions server/src/interfaces/i_spotify_service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export interface ISpotifyService {
searchVideoId(query: string): Promise<SpotifyApi.SearchResponse>
searchAlbumId(query: string): Promise<SpotifyApi.SearchResponse>
getTrackInfo(id: string): Promise<SpotifyApi.SingleTrackResponse>
getAlbumInfo(id: string): Promise<SpotifyApi.SingleAlbumResponse>
}
4 changes: 4 additions & 0 deletions server/src/interfaces/i_youtube_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@ export interface IYoutubeService {
searchVideoId(
query: string
): Promise<youtube_v3.Schema$ResourceId | undefined>
searchAlbumId(
query: string
): Promise<youtube_v3.Schema$ResourceId | undefined>
getTrackInfo(id: string): Promise<youtube_v3.Schema$Video | undefined>
getAlbumInfo(id: string): Promise<youtube_v3.Schema$Playlist | undefined>
}
71 changes: 69 additions & 2 deletions server/src/services/conversion_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ISpotifyService } from '../interfaces/i_spotify_service'
import { IYoutubeService } from '../interfaces/i_youtube_service'
import { TYPES } from '../utils/constants'
import { Logger } from '../utils/logger'
import { Match } from '../utils/is_youtube_or_spotify'

@injectable()
export class ConversionService {
Expand All @@ -16,7 +17,7 @@ export class ConversionService {
private readonly youtubeService: IYoutubeService
) {}

async convertYoutubeUrl(id: string) {
async convertYoutubeTrack(id: string) {
if (!id) {
throw new HttpException(400, 'Invalid Youtube track url')
}
Expand Down Expand Up @@ -56,7 +57,47 @@ export class ConversionService {
return spotifyTrackUrl
}

async convertSpotifyUrl(id: string) {
async convertYoutubeAlbum(id: string) {
if (!id) {
throw new HttpException(400, 'Invalid Youtube album url')
}

const youtubeAlbum = await this.youtubeService.getAlbumInfo(id)

if (youtubeAlbum === undefined) {
throw new HttpException(400, 'Invalid Youtube album url')
}

const albumTitle = youtubeAlbum.snippet?.title

if (!albumTitle) {
throw new HttpException(400, 'Invalid Youtube album url')
}

const channelTitle = youtubeAlbum.snippet?.channelTitle

if (!channelTitle) {
throw new HttpException(400, 'Invalid Youtube album url')
}

const spotifyResult = await this.spotifyService.searchAlbumId(
`${albumTitle} ${channelTitle}`
)

const spotifyAlbum = spotifyResult.albums?.items[0]

if (!spotifyAlbum) {
throw new HttpException(404, 'Album not found')
}

const spotifyAlbumId = spotifyAlbum.id

const spotifyAlbumUrl = `https://open.spotify.com/album/${spotifyAlbumId}`

return spotifyAlbumUrl
}

async convertSpotifyTrack(id: string) {
if (!id) {
throw new HttpException(400, 'Invalid Spotify track url')
}
Expand All @@ -81,4 +122,30 @@ export class ConversionService {

return youtubeVideoUrl
}

async convertSpotifyAlbum(id: string) {
if (!id) {
throw new HttpException(400, 'Invalid Spotify album url')
}

const spotifyAlbum = await this.spotifyService.getAlbumInfo(id)

const artistsString = spotifyAlbum.artists
.map((artist) => artist.name)
.join(' ')

const youtubePlaylist = await this.youtubeService.searchAlbumId(
`${spotifyAlbum.name} ${artistsString}`
)

const youtubePlaylistId = youtubePlaylist?.playlistId

if (youtubePlaylistId === undefined || youtubePlaylistId === null) {
throw new HttpException(404, 'Youtube playlist not found')
}

const youtubePlaylistUrl = `https://www.youtube.com/playlist?list=${youtubePlaylistId}`

return youtubePlaylistUrl
}
}
12 changes: 12 additions & 0 deletions server/src/services/spotify_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,21 @@ export class SpotifyService implements ISpotifyService {
return res.body
}

public readonly searchAlbumId = async (query: string) => {
const res = await this.spotifyApi.search(query, ['album'], { limit: 1 })

return res.body
}

public readonly getTrackInfo = async (id: string) => {
const res = await this.spotifyApi.getTrack(id)

return res.body
}

public readonly getAlbumInfo = async (id: string) => {
const res = await this.spotifyApi.getAlbum(id)

return res.body
}
}
35 changes: 35 additions & 0 deletions server/src/services/youtube_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,39 @@ export class YoutubeService implements IYoutubeService {

return res[0]
}

public readonly searchAlbumId = async (query: string) => {
const res = await this.youtubeApi.search
.list({
auth: process.env.YOUTUBE_API_KEY,
part: ['snippet'],
q: query,
maxResults: 1,
type: ['playlist'],
})
.then((v) => v.data.items ?? [])

if (res.length === 0) {
return undefined
}

return res[0].id
}

public readonly getAlbumInfo = async (id: string) => {
const res = await this.youtubeApi.playlists
.list({
id: [id],
part: ['snippet'],
auth: process.env.YOUTUBE_API_KEY,
maxResults: 1,
})
.then((v) => v.data.items ?? [])

if (res.length === 0) {
return undefined
}

return res[0]
}
}
76 changes: 59 additions & 17 deletions server/src/utils/is_youtube_or_spotify.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,67 @@
export const youtubeRegexes = [/.*(youtu\.be|youtube.com|music.youtube.com)\/watch\?v=((\w|-)+)?(?=(\&|$))/, /.*(youtu\.be|youtube.com)\/((\w|-)+)?(?=(\?|$))/]
export const spotifyRegex = /.*open.spotify.com\/track\/(\w+)?(?=\?|$)/
export interface Match {
type: 'spotify' | 'youtube'
id: string
kind: 'track' | 'album'
}

interface Match {
type: 'spotify' | 'youtube';
id: string;
interface Matcher {
regex: RegExp[]
idIndex: number
match: Omit<Match, 'id'>
}

export const isYoutubeOrSpotify = (url: string): Match | null => {
const spotifyMatch = url.match(spotifyRegex)
if (spotifyMatch != null && spotifyMatch.length > 1) {
return {
type: 'spotify',
id: spotifyMatch[1]
}
}
let youtubeMatch = url.match(youtubeRegexes[0]) ?? url.match(youtubeRegexes[1])
if (youtubeMatch != null && youtubeMatch.length > 1) {
return {
const matches: Matcher[] = [
{
regex: [
/.*(youtu\.be|youtube.com|music.youtube.com)\/watch\?v=((\w|-)+)?(?=(\&|$))/,
/.*(youtu\.be|youtube.com)\/((?!playlist)(\w|-)+)?(?=(\?|$))/,
],
idIndex: 2,
match: {
type: 'youtube',
kind: 'track',
},
},
{
regex: [
/.*(youtube.com|music.youtube.com)\/playlist\?list=((\w|-)+)?(?=(\&|$))/,
],
idIndex: 2,
match: {
type: 'youtube',
id: youtubeMatch[2]
kind: 'album',
},
},
{
regex: [/.*open.spotify.com\/track\/(\w+)?(?=\?|$)/],
idIndex: 1,
match: {
type: 'spotify',
kind: 'track',
},
},
{
regex: [/.*open.spotify.com\/album\/(\w+)?(?=\?|$)/],
idIndex: 1,
match: {
type: 'spotify',
kind: 'album',
},
},
]

export const isYoutubeOrSpotify = (url: string): Match | null => {
for (const matcher of matches) {
const match = matcher.regex
.map((regex) => url.match(regex))
.find((v) => v != null)
if (!!match && match.length > 1) {
return {
...matcher.match,
id: match[matcher.idIndex],
}
}
}

return null
}

0 comments on commit fb1d97a

Please sign in to comment.