Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

324 custom statistics calculation #325

Merged
merged 19 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
eb5706c
chore(deps): update nestjs to `10.3.10`
Mnigos Jul 2, 2024
635c99d
fix(modules/users/guards/check-user-id): check if `id` is a UUID
Mnigos Jul 2, 2024
3737173
feat(modules/stats): add required `dtos`, `decorators` and `enums`
Mnigos Jul 2, 2024
e7ee5ba
refactor(common/utils/rget-most-frequent-item): use lodash functions
Mnigos Jul 2, 2024
19d2f50
feat(common/utils): create `getMostListenedTracksByDuration` function
Mnigos Jul 2, 2024
3f6f1f2
feat(modules/stats): create `stats` module and implement `getTopTrack…
Mnigos Jul 2, 2024
7b7a436
fix(main): add `enableImplicitConversion` validationPipe transform op…
Mnigos Jul 2, 2024
0af7596
feat(modules/stats/types): create `TopItem` type'
Mnigos Jul 3, 2024
c9f169e
refactor(common/utils/get-most-frequent-items): return item count
Mnigos Jul 3, 2024
754e920
refactor(common/utils/get-most-listened-track-from-duration): return …
Mnigos Jul 3, 2024
cdbb25b
refactor(modules/history/tracks/repository): create `findByUserAndBet…
Mnigos Jul 3, 2024
d0154a6
refactor(common/utils): rename `getMostListenedTracksByDuration` -> `…
Mnigos Jul 3, 2024
c272c82
feat(modules/stats): implement `getTopArtists` method
Mnigos Jul 3, 2024
339d445
feat(modules/stats): implement `getTopAlbums` method
Mnigos Jul 3, 2024
49a5709
feat(modules/stats): implement `getTopGenres` method
Mnigos Jul 3, 2024
8b4878a
fix(modules/history/tracks/repository): searching between dates
Mnigos Jul 3, 2024
878b8d8
perf(modules/history/tracks/repository): pass relations through argum…
Mnigos Jul 3, 2024
cd59f58
docs(modules/stats/router): create response documents for endpoints
Mnigos Jul 3, 2024
a707a6c
docs(modules/stats/router/controller): `only one` -> `either`
Mnigos Jul 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@
"@nestjs/axios": "3.0.2",
"@nestjs/bull": "^10.1.1",
"@nestjs/cache-manager": "^2.2.2",
"@nestjs/common": "^10.3.9",
"@nestjs/config": "3.2.2",
"@nestjs/core": "^10.3.9",
"@nestjs/platform-express": "^10.3.9",
"@nestjs/common": "^10.3.10",
"@nestjs/config": "3.2.3",
"@nestjs/core": "^10.3.10",
"@nestjs/platform-express": "^10.3.10",
"@nestjs/schedule": "4.0.2",
"@nestjs/swagger": "7.3.1",
"@nestjs/swagger": "7.4.0",
"@nestjs/typeorm": "10.0.2",
"@spotify/web-api-ts-sdk": "1.2.0",
"@vitest/coverage-v8": "1.6.0",
Expand All @@ -69,26 +69,28 @@
"exponential-backoff": "3.1.1",
"ioredis": "^5.4.1",
"joi": "17.13.1",
"lodash": "^4.17.21",
"ms": "2.1.3",
"nestjs-paginate": "^8.6.2",
"pactum": "3.7.0",
"pg": "8.12.0",
"reflect-metadata": "^0.2.2",
"reflect-metadata": "0.2.2",
"rxjs": "^7.8.1",
"typeorm": "0.3.20"
},
"devDependencies": {
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@commitlint/types": "^19.0.3",
"@nestjs/cli": "^10.3.2",
"@nestjs/cli": "^10.4.0",
"@nestjs/schematics": "^10.1.1",
"@nestjs/testing": "^10.3.9",
"@swc/cli": "0.3.12",
"@swc/core": "1.5.28",
"@swc/cli": "0.4.0",
"@swc/core": "1.6.6",
"@trilon/eslint-plugin": "^0.2.1",
"@types/cookie-parser": "1.4.7",
"@types/express": "^4.17.21",
"@types/lodash": "^4.17.6",
"@types/ms": "^0.7.34",
"@types/node": "20.10.0",
"@types/passport-jwt": "4.0.1",
Expand Down
2 changes: 1 addition & 1 deletion src/common/adapters/genres.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class GenresAdapter {
genres: getMostFrequentItems(
artists.flatMap(({ genres }) => genres),
limit
),
).map(({ item }) => item),
}
}
}
12 changes: 6 additions & 6 deletions src/common/mocks/genres.mock.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Genres } from '../types/spotify'

export const topGenresArrayMock = [
"black 'n' roll",
'black metal',
'blackened crust',
'metal',
'norwegian black metal',
'norwegian death metal',
'norwegian metal',
'norwegian death metal',
'norwegian black metal',
'metal',
'blackened crust',
'black metal',
"black 'n' roll",
]

export const topGenresMock: Genres = {
Expand Down
26 changes: 21 additions & 5 deletions src/common/utils/get-most-frequent-items.util.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { getMostFrequentItems } from '.'
import { getMostFrequentItems } from './get-most-frequent-items.util'

describe('GetMostFrequentItems', () => {
test('should get most frequent item', () => {
expect(getMostFrequentItems(['a', 'a', 'b', 'c', 'c', 'c'])).toEqual(['c'])
expect(getMostFrequentItems(['a', 'a', 'b', 'c', 'c', 'c'])).toEqual([
{
item: 'c',
count: 3,
},
])
})

test('should get most frequent items', () => {
expect(getMostFrequentItems(['a', 'a', 'b', 'c', 'c', 'c'], 2)).toEqual([
'c',
'a',
{
item: 'c',
count: 3,
},
{
item: 'a',
count: 2,
},
])
})

Expand All @@ -17,6 +28,11 @@ describe('GetMostFrequentItems', () => {
})

test('should return first item, because there are no repeated items', () => {
expect(getMostFrequentItems(['a', 'b', 'c'])).toEqual(['a'])
expect(getMostFrequentItems(['a', 'b', 'c'])).toEqual([
{
item: 'c',
count: 1,
},
])
})
})
41 changes: 17 additions & 24 deletions src/common/utils/get-most-frequent-items.util.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
export function getMostFrequentItems(array: string[], limit = 1) {
if (array.length === 0) return array
import { countBy, entries, sortBy } from 'lodash'

const frequencies = {}

for (const item of array) {
frequencies[item] =
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
frequencies[item] === undefined ? 1 : frequencies[item] + 1
}

const frequencyArray = []

for (const key in frequencies) {
frequencyArray.push([frequencies[key], key] as never)
}

frequencyArray.sort((a, b) => {
return b[0] - a[0]
})
interface FrequentItem {
item: string
count: number
}

const mostFrequentItems = []
export function getMostFrequentItems(
array: string[],
limit = 1
): FrequentItem[] {
const itemCounts = countBy(array)

for (let index = 0; index < limit; index++) {
frequencyArray[index] && mostFrequentItems.push(frequencyArray[index]?.[1])
}
const itemsWithCounts: FrequentItem[] = entries(itemCounts).map(
([item, count]) => ({
item,
count,
})
)

return mostFrequentItems
return sortBy(itemsWithCounts, 'count').reverse().slice(0, limit)
}
75 changes: 75 additions & 0 deletions src/common/utils/get-most-listened-items-by-duration.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { mock, mockDeep } from 'vitest-mock-extended'

import { getMostListenedItemsByDuration } from './get-most-listened-items-by-duration.util'

import { Track } from '@modules/items/tracks'

describe('getMostListenedItemsByDuration', () => {
const mostListenedTrackId = '1'
const mostListenedTrack = mock<Track>({
id: mostListenedTrackId,
duration: 500,
})

test('should get most listened track by duration', () => {
const tracksMock = [
mostListenedTrack,
mockDeep<Track>({
id: '2',
duration: 200,
}),
mockDeep<Track>({
id: '3',
duration: 300,
}),
]

expect(getMostListenedItemsByDuration(tracksMock, 1)).toMatchObject([
{
id: mostListenedTrackId,
totalDuration: 500,
},
])
})

test('should get 3 most listened track by duration', () => {
const secondMostListenedTrackId = '2'
const thirdMostListenedTrackId = '3'

const tracksMock = [
mostListenedTrack,
mockDeep<Track>({
id: secondMostListenedTrackId,
duration: 400,
}),
mockDeep<Track>({
id: secondMostListenedTrackId,
duration: 400,
}),
mockDeep<Track>({
id: thirdMostListenedTrackId,
duration: 600,
}),
mockDeep<Track>({
id: '4',
duration: 200,
}),
mostListenedTrack,
]

expect(getMostListenedItemsByDuration(tracksMock, 3)).toMatchObject([
{
id: mostListenedTrackId,
totalDuration: 1000,
},
{
id: secondMostListenedTrackId,
totalDuration: 800,
},
{
id: thirdMostListenedTrackId,
totalDuration: 600,
},
])
})
})
28 changes: 28 additions & 0 deletions src/common/utils/get-most-listened-items-by-duration.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { chain, sumBy } from 'lodash'

import type { Track } from '@modules/items/tracks'

interface MostListenedItem {
id: string
totalDuration: number
}

export function getMostListenedItemsByDuration(
tracks: Pick<Track, 'id' | 'duration'>[],
limit = 1
): MostListenedItem[] {
if (tracks.length === 0) {
return []
}

return chain(tracks)
.groupBy('id')
.map((groupedTracks, id) => ({
id,
totalDuration: sumBy(groupedTracks, 'duration'),
}))
.sortBy('totalDuration')
.reverse()
.take(limit)
.value()
}
1 change: 1 addition & 0 deletions src/common/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './get-most-frequent-items.util'
export * from './get-most-listened-items-by-duration.util'
export * from './catch-spotify-error'
export * from './remove-duplicates.util'
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ async function bootstrap() {
app.useGlobalPipes(
new ValidationPipe({
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
})
)
app.useGlobalInterceptors(new ClassSerializerInterceptor(reflector))
Expand Down
2 changes: 2 additions & 0 deletions src/modules/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { TracksModule } from '@modules/items/tracks'
import { HistoryModule } from '@modules/history'
import { ArtistsRouterModule } from '@modules/items/artists/router'
import { HistoryRouterModule } from '@modules/history/router'
import { StatsRouterModule } from '@modules/stats/router'

@Module({
imports: [
Expand All @@ -32,6 +33,7 @@ import { HistoryRouterModule } from '@modules/history/router'
TracksModule,
HistoryModule,
HistoryRouterModule,
StatsRouterModule,
ConfigModule.forRoot({
isGlobal: true,
envFilePath: './.env',
Expand Down
37 changes: 33 additions & 4 deletions src/modules/history/tracks/history-tracks.repository.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { DataSource } from 'typeorm'
import { And, DataSource, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'
import { Test, TestingModule } from '@nestjs/testing'
import { MockProxy, mock } from 'vitest-mock-extended'

import {
HistoryTracksRepository,
historyTracksOrder,
relations,
historyTracksRelations,
} from './history-tracks.repository'
import { HistoryTrack } from './history-track.entity'
import { CreateHistoryTrack } from './dtos'
Expand Down Expand Up @@ -62,7 +62,7 @@ describe('HistoryTracksRepository', () => {
id: userId,
},
},
relations,
relations: historyTracksRelations,
order: historyTracksOrder,
})
})
Expand All @@ -82,7 +82,36 @@ describe('HistoryTracksRepository', () => {
id: userId,
},
},
relations,
relations: historyTracksRelations,
order: historyTracksOrder,
})
})

test('should find history tracks by user and between dates', async () => {
const after = new Date()
const before = new Date()

const findSpy = vi
.spyOn(historyTracksRepository, 'find')
.mockResolvedValue(historyTracksMock)

expect(
await historyTracksRepository.findByUserAndBetweenDates(
userId,
after,
before,
historyTracksRelations
)
).toEqual(historyTracksMock)

expect(findSpy).toHaveBeenCalledWith({
where: {
user: {
id: userId,
},
playedAt: And(MoreThanOrEqual(after), LessThanOrEqual(before)),
},
relations: historyTracksRelations,
order: historyTracksOrder,
})
})
Expand Down
Loading
Loading