mirror of
https://github.com/ArthurDanjou/artdanj-api.git
synced 2026-01-14 12:14:33 +01:00
Delete: Song history
This commit is contained in:
@@ -1,37 +1,28 @@
|
|||||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||||
import {
|
import {
|
||||||
fetchTopArtist,
|
fetchTopArtist, fetchTopTracks,
|
||||||
fetchTopTrack,
|
|
||||||
getAuthorizationURI,
|
getAuthorizationURI,
|
||||||
getCurrentPlayingFromCache,
|
getCurrentPlayingFromCache,
|
||||||
getHistory,
|
|
||||||
setupSpotify,
|
setupSpotify,
|
||||||
} from 'App/Utils/SongUtils'
|
} from 'App/Utils/SongUtils'
|
||||||
import SongHistoryValidator from 'App/Validators/song/SongHistoryValidator'
|
import SongRangeValidator from 'App/Validators/song/SongRangeValidator'
|
||||||
|
|
||||||
export default class SongsController {
|
export default class SongsController {
|
||||||
public async getCurrentSong({ response }: HttpContextContract) {
|
public async getCurrentSong({ response }: HttpContextContract) {
|
||||||
return response.status(200).send(await getCurrentPlayingFromCache())
|
return response.status(200).send(await getCurrentPlayingFromCache())
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHistory({ request, response }: HttpContextContract) {
|
public async getTopTrack({ request, response }: HttpContextContract) {
|
||||||
const { range } = await request.validate(SongHistoryValidator)
|
const { range } = await request.validate(SongRangeValidator)
|
||||||
const history = await getHistory(range || 'day')
|
|
||||||
return response.status(200).send({
|
return response.status(200).send({
|
||||||
range: range || 'day',
|
tracks: await fetchTopTracks(range || 'short'),
|
||||||
history,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTopTrack({ response }: HttpContextContract) {
|
public async getTopArtist({ request, response }: HttpContextContract) {
|
||||||
|
const { range } = await request.validate(SongRangeValidator)
|
||||||
return response.status(200).send({
|
return response.status(200).send({
|
||||||
tracks: await fetchTopTrack(),
|
tracks: await fetchTopArtist(range || 'short'),
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getTopArtist({ response }: HttpContextContract) {
|
|
||||||
return response.status(200).send({
|
|
||||||
tracks: await fetchTopArtist(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
|
|
||||||
|
|
||||||
export default class Song extends BaseModel {
|
|
||||||
public static table = 'spotify_songs_history'
|
|
||||||
|
|
||||||
@column({ isPrimary: true })
|
|
||||||
public date: Date
|
|
||||||
|
|
||||||
@column()
|
|
||||||
public device_name: string
|
|
||||||
|
|
||||||
@column()
|
|
||||||
public device_type: string
|
|
||||||
|
|
||||||
@column()
|
|
||||||
public item_name: string
|
|
||||||
|
|
||||||
@column()
|
|
||||||
public item_id: string
|
|
||||||
|
|
||||||
@column()
|
|
||||||
public item_type: string
|
|
||||||
|
|
||||||
@column()
|
|
||||||
public author: string
|
|
||||||
|
|
||||||
@column()
|
|
||||||
public image: string
|
|
||||||
|
|
||||||
@column()
|
|
||||||
public duration: number
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import Logger from '@ioc:Adonis/Core/Logger'
|
|
||||||
import { getCurrentPlayingFromCache } from 'App/Utils/SongUtils'
|
|
||||||
import Song from 'App/Models/Song'
|
|
||||||
|
|
||||||
const MS = 3000 // 3 seconds
|
|
||||||
let taskId
|
|
||||||
|
|
||||||
async function LogSpotifyHistory(): Promise<void> {
|
|
||||||
const current = await getCurrentPlayingFromCache()
|
|
||||||
|
|
||||||
if (!current.is_playing) return
|
|
||||||
|
|
||||||
if (current.progress && current.progress < 5000) return
|
|
||||||
|
|
||||||
await Song.create({
|
|
||||||
date: new Date(current.started_at!),
|
|
||||||
duration: current.duration,
|
|
||||||
item_name: current.name,
|
|
||||||
item_type: current.type,
|
|
||||||
item_id: current.id,
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
@@ -62,7 +62,7 @@ interface Album {
|
|||||||
uri: string
|
uri: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Item {
|
export interface Item {
|
||||||
album: Album & { album_group: 'album' | 'single' | 'compilation' | 'appears_on' ; artists: Artist[] }
|
album: Album & { album_group: 'album' | 'single' | 'compilation' | 'appears_on' ; artists: Artist[] }
|
||||||
artists: Artist[]
|
artists: Artist[]
|
||||||
available_markets: string[]
|
available_markets: string[]
|
||||||
@@ -82,6 +82,7 @@ interface Item {
|
|||||||
type: string
|
type: string
|
||||||
uri: string
|
uri: string
|
||||||
is_local: boolean
|
is_local: boolean
|
||||||
|
device: Device
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayerResponse {
|
export interface PlayerResponse {
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
|
|||||||
import Env from '@ioc:Adonis/Core/Env'
|
import Env from '@ioc:Adonis/Core/Env'
|
||||||
import Redis from '@ioc:Adonis/Addons/Redis'
|
import Redis from '@ioc:Adonis/Addons/Redis'
|
||||||
import { SpotifyArtist, SpotifyTrack } from 'App/Types/ILocalSpotify'
|
import { SpotifyArtist, SpotifyTrack } from 'App/Types/ILocalSpotify'
|
||||||
import { Artist, InternalPlayerResponse, PlayerResponse, SpotifyToken } from 'App/Types/ISpotify'
|
import { Artist, InternalPlayerResponse, Item, PlayerResponse, SpotifyToken } from 'App/Types/ISpotify'
|
||||||
import Song from 'App/Models/Song'
|
|
||||||
import queryString from 'query-string'
|
import queryString from 'query-string'
|
||||||
import { updateGithubReadmeSpotify } from 'App/Utils/UpdateGithubReadme'
|
import { updateGithubReadmeSpotify } from 'App/Utils/UpdateGithubReadme'
|
||||||
|
|
||||||
|
type Range = 'short' | 'medium' | 'long'
|
||||||
|
|
||||||
export async function getSpotifyAccount(): Promise<SpotifyToken> {
|
export async function getSpotifyAccount(): Promise<SpotifyToken> {
|
||||||
return await Redis.exists('spotify:account')
|
return await Redis.exists('spotify:account')
|
||||||
? JSON.parse(await Redis.get('spotify:account') || '{}')
|
? JSON.parse(await Redis.get('spotify:account') || '{}')
|
||||||
@@ -117,7 +118,6 @@ export async function getCurrentPlayingFromSpotify(): Promise<InternalPlayerResp
|
|||||||
author: current_track.data.item.artists.map(artist => artist.name).join(', ') || '',
|
author: current_track.data.item.artists.map(artist => artist.name).join(', ') || '',
|
||||||
id: current_track.data.item.id,
|
id: current_track.data.item.id,
|
||||||
image: current_track.data.item.album.images[0],
|
image: current_track.data.item.album.images[0],
|
||||||
progress: current_track.data.progress_ms,
|
|
||||||
duration: current_track.data.item.duration_ms,
|
duration: current_track.data.item.duration_ms,
|
||||||
started_at: current_track.data.timestamp,
|
started_at: current_track.data.timestamp,
|
||||||
}
|
}
|
||||||
@@ -147,39 +147,56 @@ export async function updateCurrentSong(song: InternalPlayerResponse): Promise<v
|
|||||||
// todo send message to Rabbit
|
// todo send message to Rabbit
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHistory(range: 'day' | 'week' | 'month' | 'total') {
|
function getTermForRange(range: Range): String {
|
||||||
if (await Redis.exists(`spotify:history:range:${range || 'day'}`))
|
return `${range}_term`
|
||||||
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(),
|
|
||||||
expiration: new Date(new Date().setMinutes(new Date().getMinutes() + 5)).toUTCString(),
|
|
||||||
history: songs,
|
|
||||||
}), 'ex', 300)
|
|
||||||
|
|
||||||
return { history: songs }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTopArtist(): Promise<SpotifyArtist[] | { artists: string }> {
|
export async function fetchTopTracks(range: Range) {
|
||||||
|
if (await Redis.exists(`spotify:top:tracks:${range || 'short'}`))
|
||||||
|
return JSON.parse(await Redis.get(`spotify:top:tracks:${range || 'short'}`) || '{}')
|
||||||
|
|
||||||
|
const fetched_tracks = await RequestWrapper<{ items: Item[] }>(`https://api.spotify.com/v1/me/top/tracks?limit=10?range=${getTermForRange(range)}`)
|
||||||
|
|
||||||
|
const tracks: SpotifyTrack[] = []
|
||||||
|
|
||||||
|
if (fetched_tracks) {
|
||||||
|
for (const track of fetched_tracks.data.items) {
|
||||||
|
tracks.push({
|
||||||
|
author: track.artists.map(artist => artist.name).join(', ') || '',
|
||||||
|
device: {
|
||||||
|
name: track.device.name,
|
||||||
|
type: track.device.type,
|
||||||
|
},
|
||||||
|
image: track.album.images[0].url,
|
||||||
|
item: {
|
||||||
|
name: track.name,
|
||||||
|
type: track.type,
|
||||||
|
id: track.id,
|
||||||
|
},
|
||||||
|
duration: track.duration_ms,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
tracks: 'cannot_fetch_tracks',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Redis.set(`spotify:top:tracks:${range || 'short'}`, JSON.stringify({
|
||||||
|
cached: new Date().toUTCString(),
|
||||||
|
expiration: new Date(new Date().setMinutes(new Date().getMinutes() + 5)).toUTCString(),
|
||||||
|
top: tracks,
|
||||||
|
}), 'ex', 300)
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTopArtist(range: Range): Promise<SpotifyArtist[] | { artists: string }> {
|
||||||
if (await Redis.exists('spotify:top:artists'))
|
if (await Redis.exists('spotify:top:artists'))
|
||||||
return JSON.parse(await Redis.get('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/artists?limit=10')
|
const fetched_artists = await RequestWrapper<{ items: Artist[] }>(`https://api.spotify.com/v1/me/top/artists?limit=10?range=${getTermForRange(range)}`)
|
||||||
|
|
||||||
const artists: SpotifyArtist[] = []
|
const artists: SpotifyArtist[] = []
|
||||||
|
|
||||||
@@ -209,46 +226,3 @@ export async function fetchTopArtist(): Promise<SpotifyArtist[] | { artists: str
|
|||||||
|
|
||||||
return artists
|
return artists
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTopTrack(): Promise<Array<SpotifyTrack & { times: number }>> {
|
|
||||||
if (await Redis.exists('spotify:top:tracks'))
|
|
||||||
return JSON.parse(await Redis.get('spotify:top:tracks') || '{}')
|
|
||||||
|
|
||||||
// Fetch all songs
|
|
||||||
const fetched_tracks = await Song.query()
|
|
||||||
|
|
||||||
if (fetched_tracks.length <= 0)
|
|
||||||
return []
|
|
||||||
|
|
||||||
const filtered = fetched_tracks.map((track) => {
|
|
||||||
return {
|
|
||||||
...{
|
|
||||||
item: {
|
|
||||||
name: track.item_name,
|
|
||||||
type: track.item_type,
|
|
||||||
id: track.item_id,
|
|
||||||
},
|
|
||||||
device: {
|
|
||||||
name: track.device_name,
|
|
||||||
type: track.device_type,
|
|
||||||
},
|
|
||||||
duration: track.duration,
|
|
||||||
author: track.author,
|
|
||||||
image: track.image,
|
|
||||||
},
|
|
||||||
times: fetched_tracks.filter(i => i.item_id === track.item_id).length,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const sorted = filtered.sort((itemA, itemB) => itemB.times - itemA.times)
|
|
||||||
|
|
||||||
const remove_dupes = sorted.filter((thing, index, self) => index === self.findIndex(t => t.item.id === thing.item.id)).slice(0, 10)
|
|
||||||
|
|
||||||
await Redis.set('spotify:top:tracks', JSON.stringify({
|
|
||||||
cached: new Date().toUTCString(),
|
|
||||||
expiration: new Date(new Date().setMinutes(new Date().getMinutes() + 5)).toUTCString(),
|
|
||||||
top: remove_dupes,
|
|
||||||
}), 'ex', 300)
|
|
||||||
|
|
||||||
return remove_dupes
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { schema } from '@ioc:Adonis/Core/Validator'
|
import { schema } from '@ioc:Adonis/Core/Validator'
|
||||||
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
|
||||||
|
|
||||||
export default class SongHistoryValidator {
|
export default class SongRangeValidator {
|
||||||
constructor(protected ctx: HttpContextContract) {
|
constructor(protected ctx: HttpContextContract) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
range: schema.enum.optional(['day', 'week', 'month', 'total'] as const),
|
range: schema.enum.optional(['short', 'medium', 'long'] as const),
|
||||||
})
|
})
|
||||||
|
|
||||||
public messages = {
|
public messages = {
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import BaseSchema from '@ioc:Adonis/Lucid/Schema'
|
|
||||||
|
|
||||||
export default class SpotifySongsHistory extends BaseSchema {
|
|
||||||
protected tableName = 'spotify_songs_history'
|
|
||||||
|
|
||||||
public async up() {
|
|
||||||
this.schema.createTable(this.tableName, (table) => {
|
|
||||||
table.timestamp('date').primary().unique().defaultTo(this.now())
|
|
||||||
table.string('device_name').notNullable()
|
|
||||||
table.string('device_type').notNullable()
|
|
||||||
table.string('item_id').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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ApplicationContract } from '@ioc:Adonis/Core/Application'
|
import { ApplicationContract } from '@ioc:Adonis/Core/Application'
|
||||||
import Logger from '@ioc:Adonis/Core/Logger'
|
import Logger from '@ioc:Adonis/Core/Logger'
|
||||||
|
|
||||||
export default class AppProvider {
|
export default class AppProvider {
|
||||||
public static needsApplication = true
|
public static needsApplication = true
|
||||||
|
|
||||||
@@ -21,12 +20,10 @@ export default class AppProvider {
|
|||||||
const StatsTask = await import('App/Tasks/StatsTask')
|
const StatsTask = await import('App/Tasks/StatsTask')
|
||||||
const StatesTask = await import('App/Tasks/StatesTask')
|
const StatesTask = await import('App/Tasks/StatesTask')
|
||||||
const CurrentSongTask = await import('App/Tasks/CurrentSongTask')
|
const CurrentSongTask = await import('App/Tasks/CurrentSongTask')
|
||||||
const HistorySongsTask = await import('App/Tasks/HistorySongsTask')
|
|
||||||
|
|
||||||
await StatsTask.Activate()
|
await StatsTask.Activate()
|
||||||
await StatesTask.Activate()
|
await StatesTask.Activate()
|
||||||
await CurrentSongTask.Activate()
|
await CurrentSongTask.Activate()
|
||||||
await HistorySongsTask.Activate()
|
|
||||||
|
|
||||||
Logger.info('Application is ready!')
|
Logger.info('Application is ready!')
|
||||||
}
|
}
|
||||||
@@ -36,12 +33,10 @@ export default class AppProvider {
|
|||||||
const StatsTask = await import('App/Tasks/StatsTask')
|
const StatsTask = await import('App/Tasks/StatsTask')
|
||||||
const StatesTask = await import('App/Tasks/StatesTask')
|
const StatesTask = await import('App/Tasks/StatesTask')
|
||||||
const CurrentSongTask = await import('App/Tasks/CurrentSongTask')
|
const CurrentSongTask = await import('App/Tasks/CurrentSongTask')
|
||||||
const HistorySongsTask = await import('App/Tasks/HistorySongsTask')
|
|
||||||
|
|
||||||
await StatsTask.ShutDown()
|
await StatsTask.ShutDown()
|
||||||
await StatesTask.ShutDown()
|
await StatesTask.ShutDown()
|
||||||
await CurrentSongTask.ShutDown()
|
await CurrentSongTask.ShutDown()
|
||||||
await HistorySongsTask.ShutDown()
|
|
||||||
|
|
||||||
Logger.info('Application is closing. Bye...')
|
Logger.info('Application is closing. Bye...')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ Route.resource('/locations', 'LocationsController').only(['index', 'store'])
|
|||||||
|
|
||||||
Route.group(() => {
|
Route.group(() => {
|
||||||
Route.get('/', 'SongsController.getCurrentSong')
|
Route.get('/', 'SongsController.getCurrentSong')
|
||||||
Route.get('/history', 'SongsController.getHistory')
|
|
||||||
|
|
||||||
Route.get('/top/tracks', 'SongsController.getTopTrack')
|
Route.get('/top/tracks', 'SongsController.getTopTrack')
|
||||||
Route.get('/top/artists', 'SongsController.getTopArtist')
|
Route.get('/top/artists', 'SongsController.getTopArtist')
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ Route.get('/', async({ response }: HttpContextContract) => {
|
|||||||
states: `${BASE_URL}/states`,
|
states: `${BASE_URL}/states`,
|
||||||
songs: {
|
songs: {
|
||||||
current_song: `${BASE_URL}/spotify`,
|
current_song: `${BASE_URL}/spotify`,
|
||||||
history: `${BASE_URL}/spotify/history`,
|
|
||||||
top_artists: `${BASE_URL}/spotify/top/artists`,
|
top_artists: `${BASE_URL}/spotify/top/artists`,
|
||||||
top_tracks: `${BASE_URL}/spotify/top/tracks`,
|
top_tracks: `${BASE_URL}/spotify/top/tracks`,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user