Connect Athena to Spotify

This commit is contained in:
2022-01-15 19:35:43 +01:00
parent f6070b20ee
commit 240fcbac90
23 changed files with 684 additions and 99 deletions

View File

@@ -6,7 +6,8 @@
"@adonisjs/repl/build/commands", "@adonisjs/repl/build/commands",
"@adonisjs/lucid/build/commands", "@adonisjs/lucid/build/commands",
"@adonisjs/mail/build/commands", "@adonisjs/mail/build/commands",
"@adonisjs/bouncer/build/commands" "@adonisjs/bouncer/build/commands",
"pretty-list-routes/build/commands/PrettyRoute.js"
], ],
"exceptionHandlerNamespace": "App/Exceptions/Handler", "exceptionHandlerNamespace": "App/Exceptions/Handler",
"aliases": { "aliases": {

View File

@@ -37,3 +37,6 @@ SMTP_PASSWORD=
WAKATIME_USER= WAKATIME_USER=
WAKATIME_KEY= WAKATIME_KEY=
WAKATIME_ID= WAKATIME_ID=
SPOTIFY_ID=
SPOTIFY_SECRET=

View File

@@ -2,7 +2,7 @@
"commands": { "commands": {
"dump:rcfile": { "dump:rcfile": {
"settings": {}, "settings": {},
"commandPath": "@adonisjs/core/commands/DumpRc", "commandPath": "@adonisjs/core/build/commands/DumpRc",
"commandName": "dump:rcfile", "commandName": "dump:rcfile",
"description": "Dump contents of .adonisrc.json file along with defaults", "description": "Dump contents of .adonisrc.json file along with defaults",
"args": [], "args": [],
@@ -13,7 +13,7 @@
"settings": { "settings": {
"loadApp": true "loadApp": true
}, },
"commandPath": "@adonisjs/core/commands/ListRoutes", "commandPath": "@adonisjs/core/build/commands/ListRoutes",
"commandName": "list:routes", "commandName": "list:routes",
"description": "List application routes", "description": "List application routes",
"args": [], "args": [],
@@ -29,7 +29,7 @@
}, },
"generate:key": { "generate:key": {
"settings": {}, "settings": {},
"commandPath": "@adonisjs/core/commands/GenerateKey", "commandPath": "@adonisjs/core/build/commands/GenerateKey",
"commandName": "generate:key", "commandName": "generate:key",
"description": "Generate a new APP_KEY secret", "description": "Generate a new APP_KEY secret",
"args": [], "args": [],
@@ -83,7 +83,9 @@
] ]
}, },
"make:model": { "make:model": {
"settings": {}, "settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/MakeModel", "commandPath": "@adonisjs/lucid/build/commands/MakeModel",
"commandName": "make:model", "commandName": "make:model",
"description": "Make a new Lucid model", "description": "Make a new Lucid model",
@@ -314,6 +316,61 @@
"description": "Actions to implement" "description": "Actions to implement"
} }
] ]
},
"routes:pretty-list": {
"settings": {
"loadApp": true,
"stayAlive": false
},
"commandPath": "pretty-list-routes/build/commands/PrettyRoute",
"commandName": "routes:pretty-list",
"description": "",
"args": [],
"aliases": [],
"flags": [
{
"name": "verbose",
"propertyName": "verbose",
"type": "boolean",
"alias": "f",
"description": "Display more information"
},
{
"name": "reverse",
"propertyName": "reverse",
"type": "boolean",
"alias": "r",
"description": "Reverse routes display"
},
{
"name": "json",
"propertyName": "json",
"type": "boolean",
"alias": "j",
"description": "Output to JSNO"
},
{
"name": "method",
"propertyName": "methodFilter",
"type": "string",
"alias": "m",
"description": "Filter routes by method"
},
{
"name": "path",
"propertyName": "pathFilter",
"type": "string",
"alias": "p",
"description": "Filter routes by path"
},
{
"name": "name",
"propertyName": "nameFilter",
"type": "string",
"alias": "n",
"description": "Filter routes by name"
}
]
} }
}, },
"aliases": {} "aliases": {}

View File

@@ -0,0 +1,44 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import {
fetchTopArtist,
fetchTopTrack,
getAuthorizationURI,
getCurrentPlayingFromCache,
getHistory,
setupSpotify,
} from 'App/Utils/SongUtils'
import SongHistoryValidator from 'App/Validators/song/SongHistoryValidator'
export default class SongsController {
public async getCurrentSong({ response }: HttpContextContract) {
return response.status(200).send(getCurrentPlayingFromCache())
}
public async getHistory({ request, response }: HttpContextContract) {
const { range } = await request.validate(SongHistoryValidator)
const history = await getHistory(range)
return response.status(200).send({
history,
})
}
public async getTopTrack({ response }: HttpContextContract) {
return response.status(200).send({
tracks: await fetchTopTrack(),
})
}
public async getTopArtist({ response }: HttpContextContract) {
return response.status(200).send({
tracks: await fetchTopArtist(),
})
}
public async authorize({ response }: HttpContextContract) {
return response.status(200).redirect(getAuthorizationURI())
}
public async callback({ request }: HttpContextContract) {
await setupSpotify(request.param('code'))
}
}

View File

@@ -1,17 +1,16 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Redis from '@ioc:Adonis/Addons/Redis' import Redis from '@ioc:Adonis/Addons/Redis'
import StateSleepingValidator from 'App/Validators/states/StateSleepingValidator' import StateSleepingValidator from 'App/Validators/states/StateSleepingValidator'
import { getCurrentPlayingFromCache } from 'App/Utils/SongUtils'
export default class StatesController { export default class StatesController {
// Listening Music
public async index({ response }: HttpContextContract) { public async index({ response }: HttpContextContract) {
const sleeping = this.formatValue(await Redis.get('states:sleeping')) const sleeping = this.formatValue(await Redis.get('states:sleeping'))
const developing = this.formatValue(await Redis.get('states:developing')) const developing = this.formatValue(await Redis.get('states:developing'))
return response.status(200).send({ return response.status(200).send({
sleeping, sleeping,
developing, developing,
listening_music: 'Soon', listening_music: await getCurrentPlayingFromCache(),
}) })
} }

27
app/Models/Song.ts Normal file
View File

@@ -0,0 +1,27 @@
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
export default class Song extends BaseModel {
@column({ isPrimary: true })
public date: number
@column()
public device_name: string
@column()
public device_type: string
@column()
public item_name: string
@column()
public item_type: string
@column()
public author: string
@column()
public image: string
@column()
public duration: number
}

View File

@@ -0,0 +1,16 @@
import Logger from '@ioc:Adonis/Core/Logger'
import { getCurrentPlayingFromSpotify } from 'App/Utils/SongUtils'
const MS = 1000
let taskId
export async function Activate(): Promise<void> {
Logger.info(`Starting task runner for watching spotify current playing [${MS} ms]`)
await getCurrentPlayingFromSpotify()
taskId = setInterval(getCurrentPlayingFromSpotify, MS)
}
export function ShutDown(): void {
clearInterval(taskId)
Logger.info('Shutdown task runner for getting current developing state')
}

View File

@@ -0,0 +1,40 @@
import Logger from '@ioc:Adonis/Core/Logger'
import { getCurrentPlayingFromCache } from 'App/Utils/SongUtils'
import Song from 'App/Models/Song'
const MS = 10000
let taskId
async function LogSpotifyHistory(): Promise<void> {
const current = await getCurrentPlayingFromCache()
if (!current.is_playing) return
if (current.progress && current.progress < 1000) return
const last_entry = await Song.query().where('id', current.id!).orderBy('date', 'desc').first()
if (last_entry && new Date().getTime() - last_entry.duration <= new Date(last_entry.date).getTime()) return
await Song.create({
date: current.started_at,
duration: current.duration,
item_name: current.name,
item_type: current.type,
author: current.author,
device_name: current.device_name,
device_type: current.device_type,
image: current.image?.url,
})
}
export async function Activate(): Promise<void> {
Logger.info(`Starting task runner for tracking spotify listen history [${MS} ms]`)
await LogSpotifyHistory()
taskId = setInterval(LogSpotifyHistory, MS)
}
export function ShutDown(): void {
clearInterval(taskId)
Logger.info('Shutdown task runner for getting current developing state')
}

View File

@@ -1,13 +0,0 @@
import Logger from '@ioc:Adonis/Core/Logger'
const MS = 1000
export async function getCurrentPlayingMusic(): Promise<void> {
// Fetch from deezer
}
export async function Activate(): Promise<void> {
Logger.info(`Starting task runner for watching deezer current playing [${MS} ms]`)
await getCurrentPlayingMusic()
setInterval(getCurrentPlayingMusic, MS)
}

View File

@@ -1,51 +1,13 @@
import { btoa } from 'buffer'
import axios from 'axios'
import Env from '@ioc:Adonis/Core/Env'
import Logger from '@ioc:Adonis/Core/Logger' import Logger from '@ioc:Adonis/Core/Logger'
import Redis from '@ioc:Adonis/Addons/Redis' import { fetchDevelopingState } from 'App/Utils/StatesTask'
const MS = 1000 * 2 * 60 // 2 min const MS = 1000 * 2 * 60 // 2 min
let taskId let taskId
interface StatesResponse {
time: number
}
async function getCurrentTime(): Promise<void> {
try {
const response = await axios.get<{ data: StatesResponse[]}>(`https://wakatime.com/api/v1/users/${Env.get('WAKATIME_USER')}/heartbeats`, {
headers: {
Authorization: `Basic ${btoa(Env.get('WAKATIME_KEY'))}`,
},
params: {
date: new Date(),
},
})
if (response.status === 200) {
const heartbeat = response.data.data[response.data.data.length - 1]
const current_time = new Date(Date.now()).getTime() / 1000
if (heartbeat && heartbeat.time!) {
const active = current_time - heartbeat.time <= 60 * 5 // Less than 5 min.
const redis_state = await Redis.get('states:developing') === 'true'
if (redis_state !== active) {
await Redis.set('states:developing', String(active))
if (redis_state) await Redis.set('states:sleeping', 'false')
}
}
}
}
catch (error) {
Logger.error('Error while getting the states')
}
}
export async function Activate(): Promise<void> { export async function Activate(): Promise<void> {
Logger.info(`Starting task runner for getting current developing state [every ${MS} ms]`) Logger.info(`Starting task runner for getting current developing state [every ${MS} ms]`)
await getCurrentTime() await fetchDevelopingState()
taskId = setInterval(getCurrentTime, MS) taskId = setInterval(fetchDevelopingState, MS)
} }
export function ShutDown(): void { export function ShutDown(): void {

View File

@@ -0,0 +1,22 @@
export interface SpotifyArtist {
id: string
name: string
image: string
genres: string[]
popularity: number
followers: number
}
export interface SpotifyTrack {
item: {
name: string
type: string
}
device: {
name: string
type: string
}
author: string
duration: number
image: string
}

110
app/Types/ISpotify.ts Normal file
View File

@@ -0,0 +1,110 @@
export interface SpotifyToken {
access_token: string
refresh_token: string
}
interface Device {
name: string
type: 'computer' | 'smartphone' | 'speaker'
}
interface ExternalUrl {
spotify: string
}
interface ExternalId {
isrc: string
ean: string
upc: string
}
interface Image {
url: string
width: number
height: number
}
interface Restriction {
reason: 'market' | 'explicit' | 'product'
}
interface Follower {
href: string
total: number
}
export interface Artist {
external_urls: ExternalUrl
followers: Follower
genres: string[]
href: string
id: string
images: Image[]
name: string
popularity: number
type: string
uri: string
}
interface Album {
album_type: 'single' | 'album' | 'compilation'
total_tracks: number
available_markets: string[]
external_urls: ExternalUrl
href: string
id: string
images: Image[]
name: string
release_date: string
release_date_precision: 'day' | 'month' | 'year'
restrictions: Restriction
type: string
uri: string
}
interface Item {
album: Album & { album_group: 'album' | 'single' | 'compilation' | 'appears_on' ; artists: Artist[] }
artists: Artist[]
available_markets: string[]
disc_number: number
duration_ms: number
explicit: boolean
external_ids: ExternalId
external_urls: ExternalUrl
href: string
id: string
is_playable: boolean
restrictions: Restriction
name: string
popularity: number
preview_url: string
track_number: number
type: string
uri: string
is_local: boolean
}
export interface PlayerResponse {
device: Device
timestamp: number
is_playing: boolean
item: Item
progress_ms: number
shuffle_state: string
repeat_state: string
currently_playing_type: 'track' | 'episode' | 'ad' | 'unknown'
}
export interface InternalPlayerResponse {
is_playing: boolean
device_name?: string
device_type?: string
name?: string
type?: string
author?: string
id?: string
image?: Image
progress?: number
duration?: number
started_at?: number
}

16
app/Types/IStats.ts Normal file
View File

@@ -0,0 +1,16 @@
export interface Time {
hours: number
minutes: number
seconds: number
}
export interface Stats {
range: {
start: string
end: string
}
development_time: Time
commands_ran: number
builds_ran: number
}

View File

@@ -1,11 +1,237 @@
export async function getHistory(range: 'day' | 'week' | 'month') { import { readFileSync, writeFileSync } from 'fs'
return range import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import Env from '@ioc:Adonis/Core/Env'
import Redis from '@ioc:Adonis/Addons/Redis'
import { SpotifyArtist, SpotifyTrack } from 'App/Types/ILocalSpotify'
import { Artist, InternalPlayerResponse, PlayerResponse, SpotifyToken } from 'App/Types/ISpotify'
import Song from 'App/Models/Song'
export function getSpotifyAccount(): { access: string; refresh: string } {
return JSON.parse(readFileSync('.config/.spotify').toString())
} }
export async function getTopTrack() { export function getAuthorizationURI(): string {
return 0 const query = JSON.stringify({
response_type: 'code',
client_id: Env.get('SPOTIFY_ID'),
scope: encodeURIComponent('user-read-playback-state user-read-currently-playing'),
redirect_uri: `${Env.get('BASE_URL')}/spotify/callback`,
})
return `https://accounts.spotify.com/authorize?${query}`
} }
export async function GetCurrentPlaying() { export async function setupSpotify(code: string): Promise<void> {
return null const authorization_tokens: AxiosResponse<SpotifyToken> = await axios.post(
'https://accounts.spotify.com/api/token',
{
code,
grant_type: 'authorization_code',
redirect_uri: `${Env.get('BASE_URL')}/spotify/callback`,
},
{
headers: {
'Authorization': `Basic ${Buffer.from(`${Env.get('SPOTIFY_ID')}:${Env.get('SPOTIFY_SECRET')}`).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
},
)
if (authorization_tokens.status === 200) {
writeFileSync(
'.config/.spotify',
JSON.stringify({
access: authorization_tokens.data.access_token,
refresh: authorization_tokens.data.refresh_token,
}),
)
}
}
export async function regenerateTokens(): Promise<void> {
const refresh_token = getSpotifyAccount().refresh
const authorization_tokens: AxiosResponse<SpotifyToken> = await axios.post(
'https://accounts.spotify.com/api/token',
{
grant_type: 'refresh_token',
refresh_token,
},
{
headers: {
'Authorization': `Basic ${Buffer.from(`${Env.get('SPOTIFY_ID')}:${Env.get('SPOTIFY_SECRET')}`).toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
},
)
if (authorization_tokens.status === 200) {
writeFileSync(
'.config/.spotify',
JSON.stringify({
access: authorization_tokens.data.access_token,
refresh: authorization_tokens.data.refresh_token,
}),
)
}
}
async function RequestWrapper<T = never>(url: string): Promise<AxiosResponse<T>> {
let request
const options: AxiosRequestConfig = {
headers: {
Authorization: `Bearer ${getSpotifyAccount().access}`,
},
}
request = await axios.get<T>(url, options)
if (request.status === 401) {
await regenerateTokens()
request = await axios.get<T>(url, options)
}
return request
}
export async function getCurrentPlayingFromCache(): Promise<InternalPlayerResponse> {
return JSON.parse(await Redis.get('spotify:current') || '') || { is_playing: false }
}
export async function getCurrentPlayingFromSpotify(): Promise<InternalPlayerResponse> {
const current_track = await RequestWrapper<PlayerResponse>('https://api.spotify.com/v1/me/player?additional_types=track,episode')
let current: InternalPlayerResponse
if (current_track.data && !['track', 'episode'].includes(current_track.data.currently_playing_type))
current = { is_playing: false }
if (current_track.data && current_track.data.is_playing) {
current = {
is_playing: true,
device_name: current_track.data.device.name,
device_type: current_track.data.device.type,
name: current_track.data.item.name,
type: current_track.data.item.type,
author: current_track.data.item.artists.map(artist => artist.name).join(', '),
id: current_track.data.item.id,
image: current_track.data.item.album.images[0],
progress: current_track.data.progress_ms,
duration: current_track.data.item.duration_ms,
started_at: current_track.data.timestamp,
}
}
else {
current = { is_playing: false }
}
if ((await Redis.get('spotify:current') as string) !== JSON.stringify(current))
await updateCurrentSong(current)
return current
}
export async function updateCurrentSong(song: InternalPlayerResponse): Promise<void> {
// const current = JSON.parse(await Redis.get('spotify/current') as string)
await Redis.set('spotify:current', JSON.stringify(song))
// const changed = diff(current, song)
// todo send message to Rabbit
}
export async function getHistory(range: 'day' | 'week' | 'month' | 'total') {
if (await Redis.exists(`spotify:history:range:${range || 'day'}`))
return JSON.parse(await Redis.get(`spotify:history:range:${range || 'day'}`) || '')
let startDate = new Date(new Date().getTime() - 24 * 60 * 60 * 1000)
if (range === 'week') startDate = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000)
else if (range === 'month') startDate = new Date(new Date().setMonth(new Date().getMonth() - 1))
const endDate = new Date()
const songs = await Song
.query()
.where('date', '<=', endDate)
.where('date', '>=', startDate)
.orderBy('date', 'desc')
if (songs.length <= 0)
return { history: 'no_tracks_in_that_range' }
await Redis.set(`spotify:history:range:${range || 'day'}`, JSON.stringify({
cached: new Date().toUTCString(),
history: songs,
}), 'ex', 300)
return { history: songs }
}
export async function fetchTopArtist(): Promise<SpotifyArtist[]> {
if (await Redis.exists('spotify:top:artists'))
return JSON.parse(await Redis.get('spotify:top:artists') || '')
const fetched_artists = await RequestWrapper<{ items: Artist[] }>('https://api.spotify.com/v1/me/top/type/artists?limit=5')
const artists: SpotifyArtist[] = []
if (fetched_artists.data) {
for (const artist of fetched_artists.data.items) {
artists.push({
id: artist.id,
image: artist.images[0].url,
name: artist.name,
followers: artist.followers.total,
genres: artist.genres,
popularity: artist.popularity,
})
}
}
else {
return []
}
await Redis.set('spotify:top:artists', JSON.stringify({
cached: new Date().toUTCString(),
top: artists,
}), 'ex', 600)
return artists
}
export async function fetchTopTrack(): Promise<SpotifyTrack[]> {
if (await Redis.exists('spotify:top:tracks'))
return JSON.parse(await Redis.get('spotify:top:tracks') || '')
const fetched_tracks = await Song
.query()
.orderBy('date', 'desc')
.limit(5)
const tracks: SpotifyTrack[] = []
if (fetched_tracks.length >= 0) {
for (const track of fetched_tracks) {
tracks.push({
item: {
name: track.item_name,
type: track.item_type,
},
device: {
name: track.device_name,
type: track.device_type,
},
duration: track.duration,
author: track.author,
image: track.image,
})
}
}
else {
return []
}
await Redis.set('spotify:top:tracks', JSON.stringify({
cached: new Date().toUTCString(),
top: tracks,
}), 'ex', 300)
return tracks
} }

40
app/Utils/StatesTask.ts Normal file
View File

@@ -0,0 +1,40 @@
import { btoa } from 'buffer'
import axios from 'axios'
import Env from '@ioc:Adonis/Core/Env'
import Redis from '@ioc:Adonis/Addons/Redis'
import Logger from '@ioc:Adonis/Core/Logger'
interface StatesResponse {
time: number
}
export async function fetchDevelopingState(): Promise<void> {
try {
const response = await axios.get<{ data: StatesResponse[]}>(`https://wakatime.com/api/v1/users/${Env.get('WAKATIME_USER')}/heartbeats`, {
headers: {
Authorization: `Basic ${btoa(Env.get('WAKATIME_KEY'))}`,
},
params: {
date: new Date(),
},
})
if (response.status === 200) {
const heartbeat = response.data.data[response.data.data.length - 1]
const current_time = new Date(Date.now()).getTime() / 1000
if (heartbeat && heartbeat.time!) {
const active = current_time - heartbeat.time <= 60 * 5 // Less than 5 min.
const redis_state = await Redis.get('states:developing') === 'true'
if (redis_state !== active) {
await Redis.set('states:developing', String(active))
if (redis_state) await Redis.set('states:sleeping', 'false')
}
}
}
}
catch (error) {
Logger.error('Error while getting the states')
}
}

View File

@@ -1,23 +1,7 @@
import DevelopmentHour from 'App/Models/DevelopmentHour' import DevelopmentHour from 'App/Models/DevelopmentHour'
import CommandsRun from 'App/Models/CommandsRun' import CommandsRun from 'App/Models/CommandsRun'
import BuildsRun from 'App/Models/BuildsRun' import BuildsRun from 'App/Models/BuildsRun'
import { Stats, Time } from 'App/Types/IStats'
interface Time {
hours: number
minutes: number
seconds: number
}
export interface Stats {
range: {
start: string
end: string
}
development_time: Time
commands_ran: number
builds_ran: number
}
function formatDate(date: Date): string { function formatDate(date: Date): string {
return date.toISOString().split('T')[0] return date.toISOString().split('T')[0]

View File

@@ -0,0 +1,15 @@
import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SongHistoryValidator {
constructor(protected ctx: HttpContextContract) {
}
public schema = schema.create({
range: schema.enum(['day', 'week', 'month', 'total'] as const),
})
public messages = {
required: 'The field {{field}} is required',
}
}

View File

@@ -0,0 +1,22 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema'
export default class SpotifySongsHistory extends BaseSchema {
protected tableName = 'spotify_songs_history'
public async up() {
this.schema.alterTable(this.tableName, (table) => {
table.timestamp('date').primary()
table.string('device_name').notNullable()
table.string('device_type').notNullable()
table.string('item_name').notNullable()
table.string('item_type').notNullable()
table.string('author').notNullable()
table.string('image').notNullable()
table.bigInteger('duration').notNullable()
})
}
public async down() {
this.schema.dropTable(this.tableName)
}
}

18
env.ts
View File

@@ -1,17 +1,3 @@
/*
|--------------------------------------------------------------------------
| Validating Environment Variables
|--------------------------------------------------------------------------
|
| In this file we define the rules for validating environment variables.
| By performing validation we ensure that your application is running in
| a stable environment with correct configuration values.
|
| This file is read automatically by the framework during the boot lifecycle
| and hence do not rename or move this file to a different location.
|
*/
import Env from '@ioc:Adonis/Core/Env' import Env from '@ioc:Adonis/Core/Env'
export default Env.rules({ export default Env.rules({
@@ -61,4 +47,8 @@ export default Env.rules({
WAKATIME_USER: Env.schema.string(), WAKATIME_USER: Env.schema.string(),
WAKATIME_KEY: Env.schema.string(), WAKATIME_KEY: Env.schema.string(),
WAKATIME_ID: Env.schema.string(), WAKATIME_ID: Env.schema.string(),
// Spotify
SPOTIFY_ID: Env.schema.string(),
SPOTIFY_SECRET: Env.schema.string(),
}) })

View File

@@ -22,10 +22,12 @@
"@adonisjs/session": "^6.1.2", "@adonisjs/session": "^6.1.2",
"@adonisjs/view": "^6.1.1", "@adonisjs/view": "^6.1.1",
"axios": "^0.22.0", "axios": "^0.22.0",
"deep-object-diff": "^1.1.0",
"luxon": "^1.27.0", "luxon": "^1.27.0",
"mjml": "^4.10.1", "mjml": "^4.10.1",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"phc-argon2": "^1.1.1", "phc-argon2": "^1.1.1",
"pretty-list-routes": "^0.0.5",
"proxy-addr": "^2.0.7", "proxy-addr": "^2.0.7",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"tslib": "^2.3.1" "tslib": "^2.3.1"

View File

@@ -21,6 +21,17 @@ Route.group(() => {
Route.post('/commands', 'StatsController.incrementCommandCount') Route.post('/commands', 'StatsController.incrementCommandCount')
Route.post('/builds', 'StatsController.incrementBuildCount') Route.post('/builds', 'StatsController.incrementBuildCount')
}).prefix('stats') }).prefix('stats')
Route.group(() => {
Route.get('/', 'SongsController.getCurrentSong')
Route.get('/history', 'SongsController.getHistory')
Route.get('/top/track', 'SongsController.getTopTrack')
Route.get('/top/artist', 'SongsController.getTopArtist')
Route.get('/authorize', 'SongsController.authorize')
Route.get('/callback', 'SongsController.callback')
}).prefix('spotify')
}).middleware('auth:web,api') }).middleware('auth:web,api')
Route.get('/files/:filename', async({ response, params }) => { Route.get('/files/:filename', async({ response, params }) => {

View File

@@ -15,6 +15,7 @@ Route.get('/', async({ response }: HttpContextContract) => {
profile: `${BASE_URL}/me`, profile: `${BASE_URL}/me`,
stats: `${BASE_URL}/stats`, stats: `${BASE_URL}/stats`,
states: `${BASE_URL}/states`, states: `${BASE_URL}/states`,
songs: `${BASE_URL}/spotify`,
locations: `${BASE_URL}/locations`, locations: `${BASE_URL}/locations`,
}, },
}) })

View File

@@ -2028,6 +2028,11 @@ deep-is@^0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
deep-object-diff@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.0.tgz#d6fabf476c2ed1751fc94d5ca693d2ed8c18bc5a"
integrity sha512-b+QLs5vHgS+IoSNcUE4n9HP2NwcHj7aqnJWsjPtuG75Rh5TOaGt0OjAYInh77d5T16V5cRDC+Pw/6ZZZiETBGw==
defer-to-connect@^2.0.0: defer-to-connect@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587"
@@ -5345,6 +5350,11 @@ pretty-hrtime@^1.0.3:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
pretty-list-routes@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/pretty-list-routes/-/pretty-list-routes-0.0.5.tgz#843b0d4445c10f9f0f967a4dbadbd8e60ecfe169"
integrity sha512-+BI6MZ3GeZlH9OdDCbHuqNHHHy1SbhyAlABKzdf45PV4xM28fECdIh2uhG7ucw5XOoqOwJmyY5Jv+jXP/CfUIg==
process-nextick-args@~2.0.0: process-nextick-args@~2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"