From cbad29dfd993f44fa61b45ff6c71b251fd54ddf4 Mon Sep 17 00:00:00 2001 From: Arthur DANJOU Date: Thu, 14 Apr 2022 23:25:29 +0200 Subject: [PATCH] Delete: Song history --- app/Controllers/Http/SongsController.ts | 25 ++-- app/Models/Song.ts | 32 ----- app/Tasks/HistorySongsTask.ts | 37 ------ app/Types/ISpotify.ts | 3 +- app/Utils/SongUtils.ts | 122 +++++++----------- ...toryValidator.ts => SongRangeValidator.ts} | 4 +- .../1642256040742_spotify_songs_history.ts | 23 ---- providers/AppProvider.ts | 5 - start/routes/api.ts | 1 - start/routes/home.ts | 1 - 10 files changed, 60 insertions(+), 193 deletions(-) delete mode 100644 app/Models/Song.ts delete mode 100644 app/Tasks/HistorySongsTask.ts rename app/Validators/song/{SongHistoryValidator.ts => SongRangeValidator.ts} (70%) delete mode 100644 database/migrations/1642256040742_spotify_songs_history.ts diff --git a/app/Controllers/Http/SongsController.ts b/app/Controllers/Http/SongsController.ts index 47c6304..a7b245f 100644 --- a/app/Controllers/Http/SongsController.ts +++ b/app/Controllers/Http/SongsController.ts @@ -1,37 +1,28 @@ import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' import { - fetchTopArtist, - fetchTopTrack, + fetchTopArtist, fetchTopTracks, getAuthorizationURI, getCurrentPlayingFromCache, - getHistory, setupSpotify, } from 'App/Utils/SongUtils' -import SongHistoryValidator from 'App/Validators/song/SongHistoryValidator' +import SongRangeValidator from 'App/Validators/song/SongRangeValidator' export default class SongsController { public async getCurrentSong({ response }: HttpContextContract) { return response.status(200).send(await getCurrentPlayingFromCache()) } - public async getHistory({ request, response }: HttpContextContract) { - const { range } = await request.validate(SongHistoryValidator) - const history = await getHistory(range || 'day') + public async getTopTrack({ request, response }: HttpContextContract) { + const { range } = await request.validate(SongRangeValidator) return response.status(200).send({ - range: range || 'day', - history, + tracks: await fetchTopTracks(range || 'short'), }) } - public async getTopTrack({ response }: HttpContextContract) { + public async getTopArtist({ request, response }: HttpContextContract) { + const { range } = await request.validate(SongRangeValidator) return response.status(200).send({ - tracks: await fetchTopTrack(), - }) - } - - public async getTopArtist({ response }: HttpContextContract) { - return response.status(200).send({ - tracks: await fetchTopArtist(), + tracks: await fetchTopArtist(range || 'short'), }) } diff --git a/app/Models/Song.ts b/app/Models/Song.ts deleted file mode 100644 index 66b9508..0000000 --- a/app/Models/Song.ts +++ /dev/null @@ -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 -} diff --git a/app/Tasks/HistorySongsTask.ts b/app/Tasks/HistorySongsTask.ts deleted file mode 100644 index 4c7b9a1..0000000 --- a/app/Tasks/HistorySongsTask.ts +++ /dev/null @@ -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 { - 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 { - 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') -} diff --git a/app/Types/ISpotify.ts b/app/Types/ISpotify.ts index b7a1975..5406ed9 100644 --- a/app/Types/ISpotify.ts +++ b/app/Types/ISpotify.ts @@ -62,7 +62,7 @@ interface Album { uri: string } -interface Item { +export interface Item { album: Album & { album_group: 'album' | 'single' | 'compilation' | 'appears_on' ; artists: Artist[] } artists: Artist[] available_markets: string[] @@ -82,6 +82,7 @@ interface Item { type: string uri: string is_local: boolean + device: Device } export interface PlayerResponse { diff --git a/app/Utils/SongUtils.ts b/app/Utils/SongUtils.ts index a4dc9b1..669d934 100644 --- a/app/Utils/SongUtils.ts +++ b/app/Utils/SongUtils.ts @@ -2,11 +2,12 @@ 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' +import { Artist, InternalPlayerResponse, Item, PlayerResponse, SpotifyToken } from 'App/Types/ISpotify' import queryString from 'query-string' import { updateGithubReadmeSpotify } from 'App/Utils/UpdateGithubReadme' +type Range = 'short' | 'medium' | 'long' + export async function getSpotifyAccount(): Promise { return await Redis.exists('spotify:account') ? JSON.parse(await Redis.get('spotify:account') || '{}') @@ -117,7 +118,6 @@ export async function getCurrentPlayingFromSpotify(): Promise 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, } @@ -147,39 +147,56 @@ export async function updateCurrentSong(song: InternalPlayerResponse): Promise=', 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 } +function getTermForRange(range: Range): String { + return `${range}_term` } -export async function fetchTopArtist(): Promise { +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 { 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/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[] = [] @@ -209,46 +226,3 @@ export async function fetchTopArtist(): Promise> { - 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 -} diff --git a/app/Validators/song/SongHistoryValidator.ts b/app/Validators/song/SongRangeValidator.ts similarity index 70% rename from app/Validators/song/SongHistoryValidator.ts rename to app/Validators/song/SongRangeValidator.ts index e524f5e..43bf09b 100644 --- a/app/Validators/song/SongHistoryValidator.ts +++ b/app/Validators/song/SongRangeValidator.ts @@ -1,12 +1,12 @@ import { schema } from '@ioc:Adonis/Core/Validator' import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' -export default class SongHistoryValidator { +export default class SongRangeValidator { constructor(protected ctx: HttpContextContract) { } 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 = { diff --git a/database/migrations/1642256040742_spotify_songs_history.ts b/database/migrations/1642256040742_spotify_songs_history.ts deleted file mode 100644 index 93ace90..0000000 --- a/database/migrations/1642256040742_spotify_songs_history.ts +++ /dev/null @@ -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) - } -} diff --git a/providers/AppProvider.ts b/providers/AppProvider.ts index fe473c3..1b98005 100755 --- a/providers/AppProvider.ts +++ b/providers/AppProvider.ts @@ -1,6 +1,5 @@ import { ApplicationContract } from '@ioc:Adonis/Core/Application' import Logger from '@ioc:Adonis/Core/Logger' - export default class AppProvider { public static needsApplication = true @@ -21,12 +20,10 @@ export default class AppProvider { const StatsTask = await import('App/Tasks/StatsTask') const StatesTask = await import('App/Tasks/StatesTask') const CurrentSongTask = await import('App/Tasks/CurrentSongTask') - const HistorySongsTask = await import('App/Tasks/HistorySongsTask') await StatsTask.Activate() await StatesTask.Activate() await CurrentSongTask.Activate() - await HistorySongsTask.Activate() Logger.info('Application is ready!') } @@ -36,12 +33,10 @@ export default class AppProvider { const StatsTask = await import('App/Tasks/StatsTask') const StatesTask = await import('App/Tasks/StatesTask') const CurrentSongTask = await import('App/Tasks/CurrentSongTask') - const HistorySongsTask = await import('App/Tasks/HistorySongsTask') await StatsTask.ShutDown() await StatesTask.ShutDown() await CurrentSongTask.ShutDown() - await HistorySongsTask.ShutDown() Logger.info('Application is closing. Bye...') } diff --git a/start/routes/api.ts b/start/routes/api.ts index 3aac487..7d9c77b 100644 --- a/start/routes/api.ts +++ b/start/routes/api.ts @@ -8,7 +8,6 @@ Route.resource('/locations', 'LocationsController').only(['index', 'store']) Route.group(() => { Route.get('/', 'SongsController.getCurrentSong') - Route.get('/history', 'SongsController.getHistory') Route.get('/top/tracks', 'SongsController.getTopTrack') Route.get('/top/artists', 'SongsController.getTopArtist') diff --git a/start/routes/home.ts b/start/routes/home.ts index 486cb52..4f67ff3 100644 --- a/start/routes/home.ts +++ b/start/routes/home.ts @@ -17,7 +17,6 @@ Route.get('/', async({ response }: HttpContextContract) => { states: `${BASE_URL}/states`, songs: { current_song: `${BASE_URL}/spotify`, - history: `${BASE_URL}/spotify/history`, top_artists: `${BASE_URL}/spotify/top/artists`, top_tracks: `${BASE_URL}/spotify/top/tracks`, },