diff --git a/Dockerfile b/Dockerfile index 39e9b6e0..74bf65a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,12 +4,14 @@ WORKDIR /usr/src/app # Install dependencies COPY package*.json ./ -COPY yarn.lock ./ +COPY yarn.lock* ./ RUN yarn # Copy source COPY . . +# This line is broken, so... just remove it! +RUN sed -i '/var global = globalSelf || phxWindow || global;/d' node_modules/phoenix/priv/static/phoenix.cjs.js RUN yarn rebuild diff --git a/README.md b/README.md index 08a62658..00307c3a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ _어떤 꿈조차도 전부 이뤄질 듯한 느낌_ Gowon is a Last.FM discord bot in active development. -Check out the indexing server over at [gowon-bot/mirrorball](https://github.com/gowon-bot/mirrorball) +Check out the indexing server over at [gowon-bot/lilac](https://github.com/gowon-bot/lilac) Check out the website over at [gowon-bot/gowon.ca](https://github.com/gowon-bot/gowon.ca) @@ -20,7 +20,7 @@ Copy `config.example.json` to `config.json`, and fill in all the fields. Then, d If you want to run a development version of the bot, you can create a `docker-compose.yml.override` to specify exposed ports and a development Dockerfile (where you could setup auto-reload with nodemon). -To start docker-compose, run `docker-compose up`. Note you will have to download and build the Mirrorball docker image. (Available at [gowon-bot/mirrorball](https://github.com/gowon-bot/mirrorball)) +To start docker-compose, run `docker-compose up`. Note you will have to download and build the Mirrorball docker image. (Available at [gowon-bot/lilac](https://github.com/gowon-bot/lilac)) ## Commands list diff --git a/docker-compose.yml b/docker-compose.yml index 0ab975fa..af811463 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ # NOTE: Ports should be specified in docker-compose.override.yml -version: "3.9" services: gowon: build: . diff --git a/package.json b/package.json index 3d62e82b..81d4983f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dependencies": { "@absinthe/socket": "^0.2.1", "@absinthe/socket-apollo-link": "^0.2.1", - "@apollo/client": "^3.6.2", + "@apollo/client": "3.6.2", "@discordjs/rest": "^0.3.0", "@sentry/cli": "^2.21.2", "@sentry/node": "^7.72.0", @@ -50,7 +50,7 @@ }, "scripts": { "start": "yarn build && node dist/src/index.js", - "build": "tsc --project ./ && yarn sentry:sourcemaps", + "build": "tsc --project ./", "rebuild": "yarn clean && yarn build", "clean": "rm -rf dist/", "build:watch": "tsc -w --project ./", diff --git a/src/commands/Admin/SyncGuild.ts b/src/commands/Admin/SyncGuild.ts index 977fbfcf..49cbcb04 100644 --- a/src/commands/Admin/SyncGuild.ts +++ b/src/commands/Admin/SyncGuild.ts @@ -6,7 +6,7 @@ export default class SyncGuild extends AdminBaseCommand { idSeed = "billlie moon sua"; description = "Syncs the list of server members with Gowon"; - aliases = ["sync", "serversync", "syncserver", "syncservermembers"]; + aliases = ["serversync", "syncserver", "syncservermembers"]; usage = ""; adminCommand = true; diff --git a/src/commands/Lastfm/Account/Login.ts b/src/commands/Lastfm/Account/Login.ts index cd2132ef..2521b09c 100644 --- a/src/commands/Lastfm/Account/Login.ts +++ b/src/commands/Lastfm/Account/Login.ts @@ -1,6 +1,6 @@ import { Message } from "discord.js"; import { User } from "../../../database/entity/User"; -import { Stopwatch, sleep } from "../../../helpers"; +import { sleep } from "../../../helpers"; import { ReactionCollectorFilter, sanitizeForDiscord, @@ -9,11 +9,12 @@ import { LastfmLinks } from "../../../helpers/lastfm/LastfmLinks"; import { LilacBaseCommand } from "../../../lib/Lilac/LilacBaseCommand"; import { Payload } from "../../../lib/context/Payload"; import { Emoji, EmojiRaw } from "../../../lib/emoji/Emoji"; -import { displayLink, displayProgressBar } from "../../../lib/ui/displays"; +import { displayLink } from "../../../lib/ui/displays"; import { InfoEmbed } from "../../../lib/ui/embeds/InfoEmbed"; import { SuccessEmbed } from "../../../lib/ui/embeds/SuccessEmbed"; import { ConfirmationView } from "../../../lib/ui/views/ConfirmationView"; import { EmbedView } from "../../../lib/ui/views/EmbedView"; +import { SyncingProgressView } from "../../../lib/ui/views/SyncingProgressView"; import { LastFMSession } from "../../../services/LastFM/converters/Misc"; export default class Login extends LilacBaseCommand { @@ -61,9 +62,9 @@ export default class Login extends LilacBaseCommand { .setDescription( `Success! You've been logged in as ${sanitizeForDiscord( user.lastFMUsername - )}\n\nWould you like to index your data?` + )}\n\nWould you like to sync your Last.fm data?` ) - .setFooter(this.indexingHelp); + .setFooter(this.syncHelp); const confirmationEmbed = new ConfirmationView( this.ctx, @@ -72,47 +73,25 @@ export default class Login extends LilacBaseCommand { ); if (await confirmationEmbed.awaitConfirmation(this.ctx)) { - this.impromptuIndex(successEmbed); + this.impromptuSync(successEmbed); } } } - private async impromptuIndex(embed: EmbedView) { - await this.lilacUsersService.index(this.ctx, { discordID: this.author.id }); + private async impromptuSync(embed: EmbedView) { + await this.lilacUsersService.sync(this.ctx, { discordID: this.author.id }); - embed - .setDescription( - `Indexing...\n${displayProgressBar(0, 1, { - width: this.progressBarWidth, - })}\n*Loading...*` - ) - .editMessage(this.ctx); - - const observable = this.lilacUsersService.indexingProgress(this.ctx, { + const observable = this.lilacUsersService.syncProgress(this.ctx, { discordID: this.author.id, }); - const stopwatch = new Stopwatch().start(); - - const subscription = observable.subscribe(async (progress) => { - if (progress.page === progress.totalPages) { - await embed - .setDescription(`${Emoji.checkmark} Done!`) - .editMessage(this.ctx); - - subscription.unsubscribe(); - } else if (stopwatch.elapsedInMilliseconds >= 3000) { - await embed - .setDescription( - `Indexing... -${displayProgressBar(progress.page, progress.totalPages, { - width: this.progressBarWidth, -})} -*Page ${progress.page}/${progress.totalPages}*` - ) - .editMessage(this.ctx); - } - }); + const syncingProgressView = new SyncingProgressView( + this.ctx, + embed, + observable + ); + + syncingProgressView.subscribeToObservable(); } private async handleCreateSession( @@ -131,8 +110,6 @@ ${displayProgressBar(progress.page, progress.totalPages, { await this.handleLilacLogin(session.username, session.key); } catch (e) { - console.log(e); - return { success: false }; } diff --git a/src/commands/Lastfm/Friends/Commands/Rating.ts b/src/commands/Lastfm/Friends/Commands/Rating.ts index ee0130fc..0323487a 100644 --- a/src/commands/Lastfm/Friends/Commands/Rating.ts +++ b/src/commands/Lastfm/Friends/Commands/Rating.ts @@ -1,4 +1,3 @@ -import gql from "graphql-tag"; import { FriendsHaveNoRatingsError } from "../../../../errors/commands/friends"; import { asyncMap } from "../../../../helpers"; import { average } from "../../../../helpers/stats"; @@ -10,9 +9,9 @@ import { displayRating, } from "../../../../lib/ui/displays"; import { ServiceRegistry } from "../../../../services/ServicesRegistry"; +import { LilacRatingsService } from "../../../../services/lilac/LilacRatingsService"; import { MirrorballRateYourMusicAlbum } from "../../../../services/mirrorball/MirrorballTypes"; import { AlbumCoverService } from "../../../../services/moderation/AlbumCoverService"; -import { RatingResponse } from "../../Mirrorball/RateYourMusic/connectors"; import { FriendsChildCommand } from "../FriendsChildCommand"; const args = { @@ -29,6 +28,7 @@ export class Rating extends FriendsChildCommand { arguments = args; albumCoverService = ServiceRegistry.get(AlbumCoverService); + lilacRatingsService = ServiceRegistry.get(LilacRatingsService); async run() { const { senderRequestable, friends } = await this.getMentions({ @@ -42,40 +42,20 @@ export class Rating extends FriendsChildCommand { senderRequestable ); - const query = gql` - query friendsRatings($user: UserInput!, $album: AlbumInput!) { - ratings( - settings: { user: $user, album: $album, pageInput: { limit: 1 } } - ) { - ratings { - rating - rateYourMusicAlbum { - title - artistName - } - } - } - } - `; - const ratings: { discordID: string; rating: number | undefined; album: MirrorballRateYourMusicAlbum | undefined; }[] = await asyncMap(friends.discordIDs(), async (friendID) => { - const ratingResponse = (await this.mirrorballService.query( - this.ctx, - query, - { - user: { discordID: friendID }, - album: { name: album, artist: { name: artist } }, - } - )) as RatingResponse; + const { ratings } = await this.lilacRatingsService.ratings(this.ctx, { + user: { discordID: friendID }, + album: { name: album, artist: { name: artist } }, + }); return { discordID: friendID, - rating: ratingResponse.ratings.ratings[0]?.rating, - album: ratingResponse.ratings.ratings[0]?.rateYourMusicAlbum, + rating: ratings[0]?.rating, + album: ratings[0]?.rateYourMusicAlbum, }; }); diff --git a/src/commands/Lastfm/Indexing/Index.ts b/src/commands/Lastfm/Indexing/Index.ts deleted file mode 100644 index 4efa1fd8..00000000 --- a/src/commands/Lastfm/Indexing/Index.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Stopwatch } from "../../../helpers"; -import { LilacBaseCommand } from "../../../lib/Lilac/LilacBaseCommand"; -import { Emoji } from "../../../lib/emoji/Emoji"; -import { displayProgressBar } from "../../../lib/ui/displays"; -import { ConfirmationView } from "../../../lib/ui/views/ConfirmationView"; - -export default class Index extends LilacBaseCommand { - idSeed = "iz*one yujin"; - subcategory = "library"; - description = "Fully index, downloading all your Last.fm data"; - aliases = ["fullindex"]; - - slashCommand = true; - - async run() { - this.lilacGuildsService.addUser( - this.ctx, - this.author.id, - this.requiredGuild.id - ); - - const embed = this.minimalEmbed() - .setDescription( - "Indexing will download all your scrobbles from Last.fm. Are you sure you want to full index?" - ) - .setFooter(this.indexingHelp); - - const confirmationEmbed = new ConfirmationView(this.ctx, embed); - - if (!(await confirmationEmbed.awaitConfirmation(this.ctx))) { - return; - } - - await this.lilacUsersService.index(this.ctx, { discordID: this.author.id }); - - embed - .setDescription( - `Indexing...\n${displayProgressBar(0, 1, { - width: this.progressBarWidth, - })}\n*Loading...*` - ) - .editMessage(this.ctx); - - const observable = this.lilacUsersService.indexingProgress(this.ctx, { - discordID: this.author.id, - }); - - const stopwatch = new Stopwatch().start(); - - await this.usersService.setIndexed(this.ctx, this.author.id, false); - - const subscription = observable.subscribe(async (progress) => { - if (progress.page === progress.totalPages) { - await this.usersService.setIndexed(this.ctx, this.author.id); - await embed - .setDescription(`${Emoji.checkmark} Done!`) - .editMessage(this.ctx); - subscription.unsubscribe(); - } else if (stopwatch.elapsedInMilliseconds >= 3000) { - await embed - .setDescription( - `Indexing... -${displayProgressBar(progress.page, progress.totalPages, { - width: this.progressBarWidth, -})} -*Page ${progress.page}/${progress.totalPages}*` - ) - .editMessage(this.ctx); - - stopwatch.zero().start(); - } - }); - } -} diff --git a/src/commands/Lastfm/Indexing/Sync.ts b/src/commands/Lastfm/Indexing/Sync.ts new file mode 100644 index 00000000..a69a352e --- /dev/null +++ b/src/commands/Lastfm/Indexing/Sync.ts @@ -0,0 +1,61 @@ +import { Flag } from "../../../lib/context/arguments/argumentTypes/Flag"; +import { LilacBaseCommand } from "../../../lib/Lilac/LilacBaseCommand"; +import { ConfirmationView } from "../../../lib/ui/views/ConfirmationView"; +import { SyncingProgressView } from "../../../lib/ui/views/SyncingProgressView"; + +const args = { + force: new Flag({ + shortnames: ["f"], + longnames: ["force"], + description: "Stop your current sync and restart it", + }), +}; + +export default class Sync extends LilacBaseCommand { + idSeed = "iz*one yujin"; + subcategory = "library"; + description = "Fully syncs your Last.fm data to Gowon"; + aliases = ["fullindex", "index"]; + + arguments = args; + + slashCommand = true; + + async run() { + this.lilacGuildsService.addUser( + this.ctx, + this.author.id, + this.requiredGuild.id + ); + + const embed = this.minimalEmbed() + .setDescription( + "Syncing will download all your scrobbles from Last.fm. Are you sure you want to sync?" + ) + .setFooter(this.syncHelp); + + const confirmationEmbed = new ConfirmationView(this.ctx, embed); + + if (!(await confirmationEmbed.awaitConfirmation(this.ctx))) { + return; + } + + await this.lilacUsersService.sync( + this.ctx, + { discordID: this.author.id }, + this.parsedArguments.force + ); + + const observable = this.lilacUsersService.syncProgress(this.ctx, { + discordID: this.author.id, + }); + + const syncProgressView = new SyncingProgressView( + this.ctx, + embed, + observable + ); + + syncProgressView.subscribeToObservable(); + } +} diff --git a/src/commands/Lastfm/Indexing/Update.ts b/src/commands/Lastfm/Indexing/Update.ts index a83455fd..6a298e9f 100644 --- a/src/commands/Lastfm/Indexing/Update.ts +++ b/src/commands/Lastfm/Indexing/Update.ts @@ -1,11 +1,9 @@ -import { Stopwatch } from "../../../helpers"; import { LilacBaseCommand } from "../../../lib/Lilac/LilacBaseCommand"; import { CommandRedirect } from "../../../lib/command/Command"; import { Flag } from "../../../lib/context/arguments/argumentTypes/Flag"; import { ArgumentsMap } from "../../../lib/context/arguments/types"; -import { Emoji } from "../../../lib/emoji/Emoji"; -import { displayProgressBar } from "../../../lib/ui/displays"; -import Index from "./Index"; +import { SyncingProgressView } from "../../../lib/ui/views/SyncingProgressView"; +import Sync from "./Sync"; const args = { full: new Flag({ @@ -25,7 +23,7 @@ export default class Update extends LilacBaseCommand { arguments = args; redirects: CommandRedirect[] = [ - { when: (args) => args.full, redirectTo: Index }, + { when: (args) => args.full, redirectTo: Sync }, ]; async run() { @@ -39,36 +37,22 @@ export default class Update extends LilacBaseCommand { discordID: this.author.id, }); - const observable = this.lilacUsersService.indexingProgress(this.ctx, { + const observable = this.lilacUsersService.syncProgress(this.ctx, { discordID: this.author.id, }); const embed = this.minimalEmbed().setDescription( - "Updating your indexed data..." + "Updating your synced data..." ); await this.reply(embed); - const stopwatch = new Stopwatch().start(); - - const subscription = observable.subscribe(async (progress) => { - if (progress.page === progress.totalPages) { - await embed - .setDescription(`${Emoji.checkmark} Done!`) - .editMessage(this.ctx); - - subscription.unsubscribe(); - } else if (stopwatch.elapsedInMilliseconds >= 3000) { - const description = `Updating... - ${displayProgressBar(progress.page, progress.totalPages, { - width: this.progressBarWidth, - })} - *Page ${progress.page}/${progress.totalPages}*`; - - await embed.setDescription(description).editMessage(this.ctx); + const syncingProgressView = new SyncingProgressView( + this.ctx, + embed, + observable + ); - stopwatch.zero().start(); - } - }); + syncingProgressView.subscribeToObservable(false); } } diff --git a/src/commands/Lastfm/Mirrorball/AlbumTopTracks/AlbumTopTracks.ts b/src/commands/Lastfm/Library/AlbumTopTracks.ts similarity index 50% rename from src/commands/Lastfm/Mirrorball/AlbumTopTracks/AlbumTopTracks.ts rename to src/commands/Lastfm/Library/AlbumTopTracks.ts index da805451..9276f0a3 100644 --- a/src/commands/Lastfm/Mirrorball/AlbumTopTracks/AlbumTopTracks.ts +++ b/src/commands/Lastfm/Library/AlbumTopTracks.ts @@ -1,32 +1,23 @@ -import { NoScrobblesFromAlbumError } from "../../../../errors/commands/library"; -import { MirrorballError } from "../../../../errors/errors"; -import { bold, italic } from "../../../../helpers/discord"; -import { LastfmLinks } from "../../../../helpers/lastfm/LastfmLinks"; -import { standardMentions } from "../../../../lib/context/arguments/mentionTypes/mentions"; -import { prefabArguments } from "../../../../lib/context/arguments/prefabArguments"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { Emoji } from "../../../../lib/emoji/Emoji"; -import { MirrorballBaseCommand } from "../../../../lib/indexing/MirrorballCommands"; -import { displayNumber } from "../../../../lib/ui/displays"; -import { ScrollingListView } from "../../../../lib/ui/views/ScrollingListView"; -import { - AlbumTopTracksConnector, - AlbumTopTracksParams, - AlbumTopTracksResponse, -} from "./AlbumTopTracks.connector"; +import { NoScrobblesFromAlbumError } from "../../../errors/commands/library"; +import { bold, italic } from "../../../helpers/discord"; +import { LastfmLinks } from "../../../helpers/lastfm/LastfmLinks"; +import { LilacBaseCommand } from "../../../lib/Lilac/LilacBaseCommand"; +import { standardMentions } from "../../../lib/context/arguments/mentionTypes/mentions"; +import { prefabArguments } from "../../../lib/context/arguments/prefabArguments"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { Emoji } from "../../../lib/emoji/Emoji"; +import { displayNumber } from "../../../lib/ui/displays"; +import { ScrollingListView } from "../../../lib/ui/views/ScrollingListView"; +import { ServiceRegistry } from "../../../services/ServicesRegistry"; +import { LilacAlbumsService } from "../../../services/lilac/LilacAlbumsService"; +import { LilacTracksService } from "../../../services/lilac/LilacTracksService"; const args = { ...prefabArguments.album, ...standardMentions, } satisfies ArgumentsMap; -export default class AlbumTopTracks extends MirrorballBaseCommand< - AlbumTopTracksResponse, - AlbumTopTracksParams, - typeof args -> { - connector = new AlbumTopTracksConnector(); - +export default class AlbumTopTracks extends LilacBaseCommand { idSeed = "shasha sunhye"; aliases = ["ltt"]; @@ -37,13 +28,16 @@ export default class AlbumTopTracks extends MirrorballBaseCommand< arguments = args; + lilacTracksService = ServiceRegistry.get(LilacTracksService); + lilacAlbumsService = ServiceRegistry.get(LilacAlbumsService); + async run() { const { username, dbUser, senderRequestable, perspective } = await this.getMentions({ senderRequired: !this.parsedArguments.artist || !this.parsedArguments.album, - reverseLookup: { required: true }, - indexedRequired: true, + dbUserRequired: true, + syncedRequired: true, }); const { artist: artistName, album: albumName } = @@ -51,39 +45,37 @@ export default class AlbumTopTracks extends MirrorballBaseCommand< redirect: true, }); - const response = await this.query({ - album: { name: albumName, artist: { name: artistName } }, - user: { discordID: dbUser.discordID }, + const { trackCounts: topTracks } = await this.lilacTracksService.listCounts( + this.ctx, + { + track: { artist: { name: artistName }, album: { name: albumName } }, + users: [{ discordID: dbUser.discordID }], + } + ); + + const album = await this.lilacAlbumsService.correctAlbumName(this.ctx, { + artist: artistName, + name: albumName, }); - const errors = this.parseErrors(response); - - if (errors) { - throw new MirrorballError(errors.errors[0].message); - } - - const { topTracks, album } = response.albumTopTracks; - if (topTracks.length < 1) { throw new NoScrobblesFromAlbumError( perspective, - album.artist.name, + album.artist, album.name ); } - const totalScrobbles = topTracks.reduce((sum, t) => sum + t.playcount, 0); - const average = totalScrobbles / topTracks.length; - const embed = this.minimalEmbed() .setTitle( `${Emoji.usesIndexedDataLink} Top tracks on ${italic( album.name - )} by ${bold(album.artist.name)} for ${username}` + )} by ${bold(album.artist)} for ${username}` ) - .setURL( - LastfmLinks.libraryAlbumPage(username, album.artist.name, album.name) - ); + .setURL(LastfmLinks.libraryAlbumPage(username, album.artist, album.name)); + + const totalScrobbles = topTracks.reduce((sum, t) => sum + t.playcount, 0); + const average = totalScrobbles / topTracks.length; const simpleScrollingEmbed = new ScrollingListView(this.ctx, embed, { pageSize: 15, @@ -93,7 +85,9 @@ export default class AlbumTopTracks extends MirrorballBaseCommand< return tracks .map( (track) => - `${displayNumber(track.playcount, "play")} - ${bold(track.name)}` + `${displayNumber(track.playcount, "play")} - ${bold( + track.track.name + )}` ) .join("\n"); }, @@ -109,7 +103,7 @@ export default class AlbumTopTracks extends MirrorballBaseCommand< topTracks.length, "total track" )}, ${displayNumber( - average.toFixed(2), + average.toFixed(0), "average scrobble" )} per track` ) + "\n", diff --git a/src/commands/Lastfm/Library/ArtistTopAlbums.ts b/src/commands/Lastfm/Library/ArtistTopAlbums.ts index 7ed55c5e..c25bc254 100644 --- a/src/commands/Lastfm/Library/ArtistTopAlbums.ts +++ b/src/commands/Lastfm/Library/ArtistTopAlbums.ts @@ -1,4 +1,4 @@ -import { NoScrobblesOfAlbumError } from "../../../errors/commands/library"; +import { NoScrobblesOfAnyAlbumsFromArtistError } from "../../../errors/commands/library"; import { bold, italic } from "../../../helpers/discord"; import { LastfmLinks } from "../../../helpers/lastfm/LastfmLinks"; import { standardMentions } from "../../../lib/context/arguments/mentionTypes/mentions"; @@ -38,7 +38,7 @@ export default class ArtistTopAlbums extends LilacBaseCommand { await this.getMentions({ senderRequired: !this.parsedArguments.artist, reverseLookup: { required: true }, - indexedRequired: true, + syncedRequired: true, }); const artistName = await this.lastFMArguments.getArtist( @@ -63,7 +63,7 @@ export default class ArtistTopAlbums extends LilacBaseCommand { const topAlbums = response.albumCounts.albumCounts; if (topAlbums.length < 1) { - throw new NoScrobblesOfAlbumError(perspective, artist.name); + throw new NoScrobblesOfAnyAlbumsFromArtistError(perspective, artist.name); } const totalScrobbles = topAlbums.reduce((sum, l) => sum + l.playcount, 0); diff --git a/src/commands/Lastfm/Mirrorball/ArtistTopTracks/ArtistTopTracks.ts b/src/commands/Lastfm/Library/ArtistTopTracks.ts similarity index 54% rename from src/commands/Lastfm/Mirrorball/ArtistTopTracks/ArtistTopTracks.ts rename to src/commands/Lastfm/Library/ArtistTopTracks.ts index 5a33c392..f4350368 100644 --- a/src/commands/Lastfm/Mirrorball/ArtistTopTracks/ArtistTopTracks.ts +++ b/src/commands/Lastfm/Library/ArtistTopTracks.ts @@ -1,24 +1,20 @@ -import { NoScrobblesOfArtistError } from "../../../../errors/commands/library"; -import { MirrorballError } from "../../../../errors/errors"; -import { bold, italic } from "../../../../helpers/discord"; -import { LastfmLinks } from "../../../../helpers/lastfm/LastfmLinks"; -import { standardMentions } from "../../../../lib/context/arguments/mentionTypes/mentions"; +import { NoScrobblesOfArtistError } from "../../../errors/commands/library"; +import { bold, italic } from "../../../helpers/discord"; +import { LastfmLinks } from "../../../helpers/lastfm/LastfmLinks"; +import { standardMentions } from "../../../lib/context/arguments/mentionTypes/mentions"; import { prefabArguments, prefabFlags, -} from "../../../../lib/context/arguments/prefabArguments"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { Emoji } from "../../../../lib/emoji/Emoji"; -import { MirrorballBaseCommand } from "../../../../lib/indexing/MirrorballCommands"; -import { displayNumber } from "../../../../lib/ui/displays"; -import { ScrollingListView } from "../../../../lib/ui/views/ScrollingListView"; -import { RedirectsService } from "../../../../services/dbservices/RedirectsService"; -import { ServiceRegistry } from "../../../../services/ServicesRegistry"; -import { - ArtistTopTracksConnector, - ArtistTopTracksParams, - ArtistTopTracksResponse, -} from "./ArtistTopTracks.connector"; +} from "../../../lib/context/arguments/prefabArguments"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { Emoji } from "../../../lib/emoji/Emoji"; +import { LilacBaseCommand } from "../../../lib/Lilac/LilacBaseCommand"; +import { displayNumber } from "../../../lib/ui/displays"; +import { ScrollingListView } from "../../../lib/ui/views/ScrollingListView"; +import { RedirectsService } from "../../../services/dbservices/RedirectsService"; +import { LilacArtistsService } from "../../../services/lilac/LilacArtistsService"; +import { LilacTracksService } from "../../../services/lilac/LilacTracksService"; +import { ServiceRegistry } from "../../../services/ServicesRegistry"; const args = { ...prefabArguments.artist, @@ -26,13 +22,7 @@ const args = { ...standardMentions, } satisfies ArgumentsMap; -export default class ArtistTopTracks extends MirrorballBaseCommand< - ArtistTopTracksResponse, - ArtistTopTracksParams, - typeof args -> { - connector = new ArtistTopTracksConnector(); - +export default class ArtistTopTracks extends LilacBaseCommand { idSeed = "weeekly soojin"; aliases = ["att", "at", "iatt", "favs"]; @@ -44,13 +34,15 @@ export default class ArtistTopTracks extends MirrorballBaseCommand< arguments = args; redirectsService = ServiceRegistry.get(RedirectsService); + lilacTracksService = ServiceRegistry.get(LilacTracksService); + lilacArtistsService = ServiceRegistry.get(LilacArtistsService); async run() { const { username, senderRequestable, perspective, dbUser } = await this.getMentions({ senderRequired: !this.parsedArguments.artist, dbUserRequired: true, - indexedRequired: true, + syncedRequired: true, }); const artistName = await this.lastFMArguments.getArtist( @@ -59,33 +51,32 @@ export default class ArtistTopTracks extends MirrorballBaseCommand< { redirect: !this.parsedArguments.noRedirect } ); - const response = await this.query({ - artist: { name: artistName }, - user: { discordID: dbUser.discordID }, - }); - - const errors = this.parseErrors(response); - - if (errors) { - throw new MirrorballError(errors.errors[0].message); - } + const { trackCounts: topTracks } = + await this.lilacTracksService.listAmbiguousCounts(this.ctx, { + track: { artist: { name: artistName } }, + users: [{ discordID: dbUser.discordID }], + }); - const { topTracks, artist } = response.artistTopTracks; + const [correctedArtistName] = + await this.lilacArtistsService.correctArtistNames(this.ctx, [artistName]); if (topTracks.length < 1) { throw new NoScrobblesOfArtistError( perspective, - artist.name, + correctedArtistName, await this.redirectHelp(this.parsedArguments.artist) ); } + const embed = this.minimalEmbed() .setTitle( `${Emoji.usesIndexedDataLink} Top ${bold( - artist.name + correctedArtistName )} tracks for ${username}` ) - .setURL(LastfmLinks.libraryArtistTopTracks(username, artist.name)); + .setURL( + LastfmLinks.libraryArtistTopTracks(username, correctedArtistName) + ); const totalScrobbles = topTracks.reduce((sum, t) => sum + t.playcount, 0); const average = totalScrobbles / topTracks.length; @@ -94,11 +85,13 @@ export default class ArtistTopTracks extends MirrorballBaseCommand< pageSize: 15, items: topTracks, - pageRenderer(tracks) { - return tracks + pageRenderer(trackCounts) { + return trackCounts .map( - (track) => - `${displayNumber(track.playcount, "play")} - ${bold(track.name)}` + (trackCount) => + `${displayNumber(trackCount.playcount, "play")} - ${bold( + trackCount.track.name + )}` ) .join("\n"); }, diff --git a/src/commands/Lastfm/Library/LastScrobbledAlbum.ts b/src/commands/Lastfm/Library/LastScrobbledAlbum.ts new file mode 100644 index 00000000..bbc8b6c1 --- /dev/null +++ b/src/commands/Lastfm/Library/LastScrobbledAlbum.ts @@ -0,0 +1,77 @@ +import { NoScrobblesOfAlbumError } from "../../../errors/commands/library"; +import { CommandRequiresResyncError } from "../../../errors/user"; +import { bold, italic } from "../../../helpers/discord"; +import { convertLilacDate } from "../../../helpers/lilac"; +import { LilacBaseCommand } from "../../../lib/Lilac/LilacBaseCommand"; +import { Variation } from "../../../lib/command/Command"; +import { standardMentions } from "../../../lib/context/arguments/mentionTypes/mentions"; +import { prefabArguments } from "../../../lib/context/arguments/prefabArguments"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { Emoji } from "../../../lib/emoji/Emoji"; +import { displayDate } from "../../../lib/ui/displays"; +import { ServiceRegistry } from "../../../services/ServicesRegistry"; +import { LilacLibraryService } from "../../../services/lilac/LilacLibraryService"; + +const args = { + ...standardMentions, + ...prefabArguments.album, +} satisfies ArgumentsMap; + +export default class LastScrobbledAlbum extends LilacBaseCommand { + idSeed = "shasha chaki"; + + aliases = ["lastl", "lal", "lastalbum"]; + variations: Variation[] = [ + { name: "first", variation: ["firstl", "fl", "fal", "firstalbum"] }, + ]; + + subcategory = "library"; + description = "Shows the last time you scrobbled an album"; + + arguments = args; + + lilacLibraryService = ServiceRegistry.get(LilacLibraryService); + + async run() { + const { senderRequestable, dbUser, perspective } = await this.getMentions({ + senderRequired: + !this.parsedArguments.artist || !this.parsedArguments.album, + reverseLookup: { required: true }, + syncedRequired: true, + }); + + const { artist: artistName, album: albumName } = + await this.lastFMArguments.getAlbum(this.ctx, senderRequestable, { + redirect: true, + }); + + const albumCount = await this.lilacLibraryService.getAlbumCount( + this.ctx, + dbUser.discordID, + artistName, + albumName + ); + + if (!albumCount) { + throw new NoScrobblesOfAlbumError(perspective, artistName, albumName); + } else if (!albumCount.lastScrobbled || !albumCount.firstScrobbled) { + throw new CommandRequiresResyncError(this.prefix); + } + + const embed = this.minimalEmbed().setDescription( + `${Emoji.usesIndexedDataDescription} ${perspective.upper.name} ${ + this.variationWasUsed("first") ? "first" : "last" + } scrobbled ${italic(albumCount.album.name)} by ${bold( + albumCount.album.artist.name + )} on ${displayDate( + convertLilacDate( + this.variationWasUsed("first") + ? albumCount.firstScrobbled + : albumCount.lastScrobbled + ) + )}` + ); + + await this.reply(embed); + } +} diff --git a/src/commands/Lastfm/Library/LastScrobbledArtist.ts b/src/commands/Lastfm/Library/LastScrobbledArtist.ts new file mode 100644 index 00000000..8c1d46e0 --- /dev/null +++ b/src/commands/Lastfm/Library/LastScrobbledArtist.ts @@ -0,0 +1,79 @@ +import { NoScrobblesOfArtistError } from "../../../errors/commands/library"; +import { CommandRequiresResyncError } from "../../../errors/user"; +import { bold } from "../../../helpers/discord"; +import { convertLilacDate } from "../../../helpers/lilac"; +import { LilacBaseCommand } from "../../../lib/Lilac/LilacBaseCommand"; +import { Variation } from "../../../lib/command/Command"; +import { standardMentions } from "../../../lib/context/arguments/mentionTypes/mentions"; +import { + prefabArguments, + prefabFlags, +} from "../../../lib/context/arguments/prefabArguments"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { Emoji } from "../../../lib/emoji/Emoji"; +import { displayDate } from "../../../lib/ui/displays"; +import { ServiceRegistry } from "../../../services/ServicesRegistry"; +import { LilacArtistsService } from "../../../services/lilac/LilacArtistsService"; + +const args = { + ...standardMentions, + ...prefabArguments.artist, + noRedirect: prefabFlags.noRedirect, +} satisfies ArgumentsMap; + +export default class LastScrobbledArtist extends LilacBaseCommand { + idSeed = "shasha wanlim"; + + aliases = ["last", "lasta", "la", "lastartist"]; + + variations: Variation[] = [ + { name: "first", variation: ["first", "firsta", "fa"] }, + ]; + + subcategory = "library"; + description = "Shows the last time you scrobbled an artist"; + + arguments = args; + + lilacArtistsService = ServiceRegistry.get(LilacArtistsService); + + async run() { + const { senderRequestable, dbUser, perspective } = await this.getMentions({ + senderRequired: !this.parsedArguments.artist, + reverseLookup: { required: true }, + syncedRequired: true, + }); + + const artistName = await this.lastFMArguments.getArtist( + this.ctx, + senderRequestable, + { redirect: !this.parsedArguments.noRedirect } + ); + + const artistCount = await this.lilacArtistsService.getCount( + this.ctx, + dbUser.discordID, + artistName + ); + + if (!artistCount) { + throw new NoScrobblesOfArtistError(perspective, artistName, ""); + } else if (!artistCount.lastScrobbled || !artistCount.firstScrobbled) { + throw new CommandRequiresResyncError(this.prefix); + } + + const embed = this.minimalEmbed().setDescription( + `${Emoji.usesIndexedDataDescription} ${perspective.upper.name} ${ + this.variationWasUsed("first") ? "first" : "last" + } scrobbled ${bold(artistCount.artist.name)} on ${displayDate( + convertLilacDate( + this.variationWasUsed("first") + ? artistCount.firstScrobbled + : artistCount.lastScrobbled + ) + )}` + ); + + await this.reply(embed); + } +} diff --git a/src/commands/Lastfm/Library/LastScrobbledTrack.ts b/src/commands/Lastfm/Library/LastScrobbledTrack.ts new file mode 100644 index 00000000..5d898c70 --- /dev/null +++ b/src/commands/Lastfm/Library/LastScrobbledTrack.ts @@ -0,0 +1,75 @@ +import { NoScrobblesOfTrackError } from "../../../errors/commands/library"; +import { CommandRequiresResyncError } from "../../../errors/user"; +import { bold, italic } from "../../../helpers/discord"; +import { convertLilacDate } from "../../../helpers/lilac"; +import { LilacBaseCommand } from "../../../lib/Lilac/LilacBaseCommand"; +import { Variation } from "../../../lib/command/Command"; +import { prefabArguments } from "../../../lib/context/arguments/prefabArguments"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { Emoji } from "../../../lib/emoji/Emoji"; +import { displayDate } from "../../../lib/ui/displays"; +import { ServiceRegistry } from "../../../services/ServicesRegistry"; +import { LilacTracksService } from "../../../services/lilac/LilacTracksService"; + +const args = { + ...prefabArguments.track, +} satisfies ArgumentsMap; + +export default class LastScrobbledTrack extends LilacBaseCommand { + idSeed = "shasha hwi a"; + + aliases = ["lastt", "lasttrack", "lt"]; + variations: Variation[] = [ + { name: "first", variation: ["firstt", "ft", "firsttrack"] }, + ]; + + subcategory = "library"; + description = "Shows the last time you scrobbled a track"; + + arguments = args; + + lilacTracksService = ServiceRegistry.get(LilacTracksService); + + async run() { + const { senderRequestable, dbUser, perspective } = await this.getMentions({ + senderRequired: + !this.parsedArguments.artist || !this.parsedArguments.track, + reverseLookup: { required: true }, + syncedRequired: true, + }); + + const { artist: artistName, track: trackName } = + await this.lastFMArguments.getTrack(this.ctx, senderRequestable, { + redirect: true, + }); + + const trackCount = await this.lilacTracksService.getAmbiguousCount( + this.ctx, + dbUser.discordID, + artistName, + trackName + ); + + if (!trackCount) { + throw new NoScrobblesOfTrackError(perspective, artistName, trackName); + } else if (!trackCount.lastScrobbled || !trackCount.firstScrobbled) { + throw new CommandRequiresResyncError(this.prefix); + } + + const embed = this.minimalEmbed().setDescription( + `${Emoji.usesIndexedDataDescription} ${perspective.upper.name} ${ + this.variationWasUsed("first") ? "first" : "last" + } scrobbled ${italic(trackCount.track.name)} by ${bold( + trackCount.track.artist.name + )} on ${displayDate( + convertLilacDate( + this.variationWasUsed("first") + ? trackCount.firstScrobbled + : trackCount.lastScrobbled + ) + )}` + ); + + await this.reply(embed); + } +} diff --git a/src/commands/Lastfm/Library/Scrobble.ts b/src/commands/Lastfm/Library/Scrobble.ts index 93a0984b..9216453d 100644 --- a/src/commands/Lastfm/Library/Scrobble.ts +++ b/src/commands/Lastfm/Library/Scrobble.ts @@ -4,7 +4,7 @@ import { ArgumentsMap } from "../../../lib/context/arguments/types"; import { ConfirmationView } from "../../../lib/ui/views/ConfirmationView"; import { LastFMArgumentsMutableContext } from "../../../services/LastFM/LastFMArguments"; import { ServiceRegistry } from "../../../services/ServicesRegistry"; -import { LilacLibraryService } from "../../../services/lilac/LilacLibraryService"; +import { LilacTracksService } from "../../../services/lilac/LilacTracksService"; import { LastFMBaseCommand } from "../LastFMBaseCommand"; const args = { @@ -20,7 +20,7 @@ export default class Scrobble extends LastFMBaseCommand { arguments = args; - lilacLibraryService = ServiceRegistry.get(LilacLibraryService); + lilacTracksService = ServiceRegistry.get(LilacTracksService); async run() { const { senderRequestable, dbUser, requestable } = await this.getMentions({ @@ -55,7 +55,7 @@ export default class Scrobble extends LastFMBaseCommand { if (!confirmation) return; } - const indexedTrackCounts = await this.lilacLibraryService.trackCounts( + const indexedTrackCounts = await this.lilacTracksService.listCounts( this.ctx, { track: { name: track, artist: { name: artist } }, diff --git a/src/commands/Lastfm/Library/ScrobbleList/NoAlbums.ts b/src/commands/Lastfm/Library/ScrobbleList/NoAlbums.ts index f72b826d..6ab57256 100644 --- a/src/commands/Lastfm/Library/ScrobbleList/NoAlbums.ts +++ b/src/commands/Lastfm/Library/ScrobbleList/NoAlbums.ts @@ -42,7 +42,8 @@ export default class NoAlbums extends LilacBaseCommand { const { dbUser, perspective, username } = await this.getMentions({ senderRequired: !this.parsedArguments.artist, reverseLookup: { required: true }, - indexedRequired: true, + syncedRequired: true, + backerRequired: true, }); const timeZone = await this.timeAndDateService.saveUserTimeZoneInContext( diff --git a/src/commands/Lastfm/Library/ScrobbleList/ScrobbleList.ts b/src/commands/Lastfm/Library/ScrobbleList/ScrobbleList.ts index 144e7950..8601f5e9 100644 --- a/src/commands/Lastfm/Library/ScrobbleList/ScrobbleList.ts +++ b/src/commands/Lastfm/Library/ScrobbleList/ScrobbleList.ts @@ -36,7 +36,8 @@ export default class ScrobbleList extends LilacBaseCommand { senderRequired: !this.parsedArguments.artist || !this.parsedArguments.track, reverseLookup: { required: true }, - indexedRequired: true, + syncedRequired: true, + backerRequired: true, }); const { artist: artistName, track: trackName } = diff --git a/src/commands/Lastfm/Library/TrackTopAlbums.ts b/src/commands/Lastfm/Library/TrackTopAlbums.ts index 91dddd69..012355ca 100644 --- a/src/commands/Lastfm/Library/TrackTopAlbums.ts +++ b/src/commands/Lastfm/Library/TrackTopAlbums.ts @@ -1,4 +1,4 @@ -import { NoScrobblesForTrackError } from "../../../errors/commands/library"; +import { NoScrobblesOfTrackError } from "../../../errors/commands/library"; import { bold, italic } from "../../../helpers/discord"; import { LastfmLinks } from "../../../helpers/lastfm/LastfmLinks"; import { LilacBaseCommand } from "../../../lib/Lilac/LilacBaseCommand"; @@ -9,7 +9,7 @@ import { Emoji } from "../../../lib/emoji/Emoji"; import { displayNumber } from "../../../lib/ui/displays"; import { ScrollingListView } from "../../../lib/ui/views/ScrollingListView"; import { ServiceRegistry } from "../../../services/ServicesRegistry"; -import { LilacLibraryService } from "../../../services/lilac/LilacLibraryService"; +import { LilacTracksService } from "../../../services/lilac/LilacTracksService"; const args = { ...prefabArguments.track, @@ -27,7 +27,7 @@ export default class TrackTopAlbums extends LilacBaseCommand { arguments = args; - lilacLibraryService = ServiceRegistry.get(LilacLibraryService); + lilacTracksService = ServiceRegistry.get(LilacTracksService); async run() { const { username, dbUser, senderRequestable, perspective } = @@ -35,7 +35,7 @@ export default class TrackTopAlbums extends LilacBaseCommand { senderRequired: !this.parsedArguments.artist || !this.parsedArguments.track, reverseLookup: { required: true }, - indexedRequired: true, + syncedRequired: true, }); const { artist: artistName, track: trackName } = @@ -43,7 +43,7 @@ export default class TrackTopAlbums extends LilacBaseCommand { redirect: true, }); - const response = await this.lilacLibraryService.trackCounts(this.ctx, { + const response = await this.lilacTracksService.listCounts(this.ctx, { track: { name: trackName, artist: { name: artistName } }, users: [{ discordID: dbUser.discordID }], }); @@ -52,7 +52,7 @@ export default class TrackTopAlbums extends LilacBaseCommand { const trackCounts = response.trackCounts; if (trackCounts.length < 1) { - throw new NoScrobblesForTrackError( + throw new NoScrobblesOfTrackError( perspective, track.artist.name, track.name diff --git a/src/commands/Lastfm/Mirrorball/AlbumTopTracks/AlbumTopTracks.connector.ts b/src/commands/Lastfm/Mirrorball/AlbumTopTracks/AlbumTopTracks.connector.ts deleted file mode 100644 index 973f9bb1..00000000 --- a/src/commands/Lastfm/Mirrorball/AlbumTopTracks/AlbumTopTracks.connector.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { gql } from "apollo-server-express"; -import { BaseConnector } from "../../../../lib/indexing/BaseConnector"; -import { - AlbumInput, - MirrorballAlbum, - UserInput, -} from "../../../../services/mirrorball/MirrorballTypes"; - -export interface AlbumTopTracksResponse { - albumTopTracks: { - album: MirrorballAlbum; - - topTracks: { - playcount: number; - name: string; - }[]; - }; -} - -export interface AlbumTopTracksParams { - album: AlbumInput; - user: UserInput; -} - -export class AlbumTopTracksConnector extends BaseConnector< - AlbumTopTracksResponse, - AlbumTopTracksParams -> { - query = gql` - query albumTopTracks($album: AlbumInput!, $user: UserInput!) { - albumTopTracks(album: $album, user: $user) { - album { - name - artist { - name - } - } - - topTracks { - playcount - name - } - } - } - `; -} diff --git a/src/commands/Lastfm/Mirrorball/ArtistTopTracks/ArtistTopTracks.connector.ts b/src/commands/Lastfm/Mirrorball/ArtistTopTracks/ArtistTopTracks.connector.ts deleted file mode 100644 index 56faf46b..00000000 --- a/src/commands/Lastfm/Mirrorball/ArtistTopTracks/ArtistTopTracks.connector.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { gql } from "apollo-server-express"; -import { BaseConnector } from "../../../../lib/indexing/BaseConnector"; -import { - ArtistInput, - UserInput, -} from "../../../../services/mirrorball/MirrorballTypes"; - -export interface ArtistTopTracksResponse { - artistTopTracks: { - artist: { - name: string; - }; - - topTracks: { - playcount: number; - name: string; - }[]; - }; -} - -export interface ArtistTopTracksParams { - artist: ArtistInput; - user: UserInput; -} - -export class ArtistTopTracksConnector extends BaseConnector< - ArtistTopTracksResponse, - ArtistTopTracksParams -> { - query = gql` - query artistTopTracks($artist: ArtistInput!, $user: UserInput!) { - artistTopTracks(artist: $artist, user: $user) { - artist { - name - } - - topTracks { - playcount - name - } - } - } - `; -} diff --git a/src/commands/Lastfm/Mirrorball/LastScrobbled/LastScrobbledAlbum.ts b/src/commands/Lastfm/Mirrorball/LastScrobbled/LastScrobbledAlbum.ts deleted file mode 100644 index 2557bc28..00000000 --- a/src/commands/Lastfm/Mirrorball/LastScrobbled/LastScrobbledAlbum.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { MirrorballError } from "../../../../errors/errors"; -import { bold, italic } from "../../../../helpers/discord"; -import { convertMirrorballDate } from "../../../../helpers/mirrorball"; -import { Variation } from "../../../../lib/command/Command"; -import { prefabArguments } from "../../../../lib/context/arguments/prefabArguments"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { Emoji } from "../../../../lib/emoji/Emoji"; -import { MirrorballBaseCommand } from "../../../../lib/indexing/MirrorballCommands"; -import { displayDate } from "../../../../lib/ui/displays"; -import { - LastScrobbledConnector, - LastScrobbledParams, - LastScrobbledResponse, -} from "./connector"; - -const args = { - ...prefabArguments.album, -} satisfies ArgumentsMap; - -export default class LastScrobbledAlbum extends MirrorballBaseCommand< - LastScrobbledResponse, - LastScrobbledParams, - typeof args -> { - connector = new LastScrobbledConnector(); - - idSeed = "shasha chaki"; - - aliases = ["lastl", "lal", "lastalbum"]; - variations: Variation[] = [ - { name: "first", variation: ["firstl", "fl", "fal", "firstalbum"] }, - ]; - - subcategory = "library"; - description = "Shows the last time you scrobbled an album"; - - arguments = args; - - async run() { - const { senderRequestable, dbUser, perspective } = await this.getMentions({ - senderRequired: - !this.parsedArguments.artist || !this.parsedArguments.album, - reverseLookup: { required: true }, - indexedRequired: true, - }); - - const { artist: artistName, album: albumName } = - await this.lastFMArguments.getAlbum(this.ctx, senderRequestable, { - redirect: true, - }); - - const response = await this.query({ - track: { album: { name: albumName, artist: { name: artistName } } }, - user: { discordID: dbUser.discordID }, - sort: this.variationWasUsed("first") - ? "scrobbled_at asc" - : "scrobbled_at desc", - }); - - const errors = this.parseErrors(response); - - if (errors) { - throw new MirrorballError(errors.errors[0].message); - } - - const [play] = response.plays.plays; - - const embed = this.minimalEmbed().setDescription( - `${Emoji.usesIndexedDataDescription} ${perspective.upper.name} ${ - this.variationWasUsed("first") ? "first" : "last" - } scrobbled ${italic(play.track.album.name)} by ${bold( - play.track.artist.name - )} on ${displayDate(convertMirrorballDate(play.scrobbledAt))}` - ); - - await this.reply(embed); - } -} diff --git a/src/commands/Lastfm/Mirrorball/LastScrobbled/LastScrobbledArtist.ts b/src/commands/Lastfm/Mirrorball/LastScrobbled/LastScrobbledArtist.ts deleted file mode 100644 index 23a8bca2..00000000 --- a/src/commands/Lastfm/Mirrorball/LastScrobbled/LastScrobbledArtist.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { MirrorballError } from "../../../../errors/errors"; -import { bold } from "../../../../helpers/discord"; -import { convertMirrorballDate } from "../../../../helpers/mirrorball"; -import { Variation } from "../../../../lib/command/Command"; -import { standardMentions } from "../../../../lib/context/arguments/mentionTypes/mentions"; -import { - prefabArguments, - prefabFlags, -} from "../../../../lib/context/arguments/prefabArguments"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { Emoji } from "../../../../lib/emoji/Emoji"; -import { MirrorballBaseCommand } from "../../../../lib/indexing/MirrorballCommands"; -import { displayDate } from "../../../../lib/ui/displays"; -import { - LastScrobbledConnector, - LastScrobbledParams, - LastScrobbledResponse, -} from "./connector"; - -const args = { - ...standardMentions, - ...prefabArguments.artist, - noRedirect: prefabFlags.noRedirect, -} satisfies ArgumentsMap; - -export default class LastScrobbledArtist extends MirrorballBaseCommand< - LastScrobbledResponse, - LastScrobbledParams, - typeof args -> { - connector = new LastScrobbledConnector(); - - idSeed = "shasha wanlim"; - - aliases = ["last", "lasta", "la", "lastartist"]; - - variations: Variation[] = [ - { name: "first", variation: ["first", "firsta", "fa"] }, - ]; - - subcategory = "library"; - description = "Shows the last time you scrobbled an artist"; - - arguments = args; - - async run() { - const { senderRequestable, dbUser, perspective } = await this.getMentions({ - senderRequired: !this.parsedArguments.artist, - reverseLookup: { required: true }, - indexedRequired: true, - }); - - const artistName = await this.lastFMArguments.getArtist( - this.ctx, - senderRequestable, - { redirect: !this.parsedArguments.noRedirect } - ); - - const response = await this.query({ - track: { artist: { name: artistName } }, - user: { discordID: dbUser.discordID }, - sort: this.variationWasUsed("first") - ? "scrobbled_at asc" - : "scrobbled_at desc", - }); - - const errors = this.parseErrors(response); - - if (errors) { - throw new MirrorballError(errors.errors[0].message); - } - - const [play] = response.plays.plays; - - const embed = this.minimalEmbed().setDescription( - `${Emoji.usesIndexedDataDescription} ${perspective.upper.name} ${ - this.variationWasUsed("first") ? "first" : "last" - } scrobbled ${bold(play.track.artist.name)} on ${displayDate( - convertMirrorballDate(play.scrobbledAt) - )}` - ); - - await this.reply(embed); - } -} diff --git a/src/commands/Lastfm/Mirrorball/LastScrobbled/LastScrobbledTrack.ts b/src/commands/Lastfm/Mirrorball/LastScrobbled/LastScrobbledTrack.ts deleted file mode 100644 index 4d82bd07..00000000 --- a/src/commands/Lastfm/Mirrorball/LastScrobbled/LastScrobbledTrack.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { MirrorballError } from "../../../../errors/errors"; -import { bold, italic } from "../../../../helpers/discord"; -import { convertMirrorballDate } from "../../../../helpers/mirrorball"; -import { Variation } from "../../../../lib/command/Command"; -import { prefabArguments } from "../../../../lib/context/arguments/prefabArguments"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { Emoji } from "../../../../lib/emoji/Emoji"; -import { MirrorballBaseCommand } from "../../../../lib/indexing/MirrorballCommands"; -import { displayDate } from "../../../../lib/ui/displays"; -import { - LastScrobbledConnector, - LastScrobbledParams, - LastScrobbledResponse, -} from "./connector"; - -const args = { - ...prefabArguments.track, -} satisfies ArgumentsMap; - -export default class LastScrobbledTrack extends MirrorballBaseCommand< - LastScrobbledResponse, - LastScrobbledParams, - typeof args -> { - connector = new LastScrobbledConnector(); - - idSeed = "shasha hwi a"; - - aliases = ["lastt", "lasttrack", "lt"]; - variations: Variation[] = [ - { name: "first", variation: ["firstt", "ft", "firsttrack"] }, - ]; - - subcategory = "library"; - description = "Shows the last time you scrobbled a track"; - - arguments = args; - - async run() { - const { senderRequestable, dbUser, perspective } = await this.getMentions({ - senderRequired: - !this.parsedArguments.artist || !this.parsedArguments.track, - reverseLookup: { required: true }, - indexedRequired: true, - }); - - const { artist: artistName, track: trackName } = - await this.lastFMArguments.getTrack(this.ctx, senderRequestable, { - redirect: true, - }); - - const response = await this.query({ - track: { name: trackName, artist: { name: artistName } }, - user: { discordID: dbUser.discordID }, - sort: this.variationWasUsed("first") - ? "scrobbled_at asc" - : "scrobbled_at desc", - }); - - const errors = this.parseErrors(response); - - if (errors) { - throw new MirrorballError(errors.errors[0].message); - } - - const [play] = response.plays.plays; - - const embed = this.minimalEmbed().setDescription( - `${Emoji.usesIndexedDataDescription} ${perspective.upper.name} ${ - this.variationWasUsed("first") ? "first" : "last" - } scrobbled ${italic(play.track.name)} by ${bold( - play.track.artist.name - )} on ${displayDate(convertMirrorballDate(play.scrobbledAt))}` - ); - - await this.reply(embed); - } -} diff --git a/src/commands/Lastfm/Mirrorball/LastScrobbled/connector.ts b/src/commands/Lastfm/Mirrorball/LastScrobbled/connector.ts deleted file mode 100644 index b366c5c5..00000000 --- a/src/commands/Lastfm/Mirrorball/LastScrobbled/connector.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { gql } from "apollo-server-express"; -import { BaseConnector } from "../../../../lib/indexing/BaseConnector"; -import { - MirrorballTrack, - TrackInput, - UserInput, -} from "../../../../services/mirrorball/MirrorballTypes"; - -// LastScrobbledArtist -export interface LastScrobbledResponse { - plays: { - plays: [ - { - scrobbledAt: number; - track: MirrorballTrack; - } - ]; - }; -} - -export interface LastScrobbledParams { - user: UserInput; - track: TrackInput; - sort: `scrobbled_at ${"desc" | "asc"}`; -} - -export class LastScrobbledConnector extends BaseConnector< - LastScrobbledResponse, - LastScrobbledParams -> { - query = gql` - query lastScrobbled( - $user: UserInput! - $track: TrackInput! - $sort: String! - ) { - plays( - playsInput: { user: $user, track: $track, sort: $sort } - pageInput: { limit: 1 } - ) { - plays { - scrobbledAt - - track { - artist { - name - } - - album { - name - } - - name - } - } - } - } - `; -} diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/Rating.ts b/src/commands/Lastfm/Mirrorball/RateYourMusic/Rating.ts deleted file mode 100644 index 07108c2e..00000000 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/Rating.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { CouldNotFindRatingError } from "../../../../errors/commands/library"; -import { UnknownMirrorballError } from "../../../../errors/errors"; -import { prefabArguments } from "../../../../lib/context/arguments/prefabArguments"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { displayRating } from "../../../../lib/ui/displays"; -import { AlbumCoverService } from "../../../../services/moderation/AlbumCoverService"; -import { ServiceRegistry } from "../../../../services/ServicesRegistry"; -import { RatingConnector, RatingParams, RatingResponse } from "./connectors"; -import { RateYourMusicIndexingChildCommand } from "./RateYourMusicChildCommand"; - -const args = { - ...prefabArguments.album, -} satisfies ArgumentsMap; - -export class Rating extends RateYourMusicIndexingChildCommand< - RatingResponse, - RatingParams, - typeof args -> { - connector = new RatingConnector(); - - idSeed = "sonamoo newsun"; - description = "Shows what you've rated an album"; - - arguments = args; - - slashCommand = true; - - albumCoverService = ServiceRegistry.get(AlbumCoverService); - - async run() { - const { senderRequestable, dbUser } = await this.getMentions({ - senderRequired: - !this.parsedArguments.artist || !this.parsedArguments.album, - reverseLookup: { required: true }, - indexedRequired: true, - }); - - const { artist, album } = await this.lastFMArguments.getAlbum( - this.ctx, - senderRequestable - ); - - const response = await this.query({ - user: { - lastFMUsername: dbUser.lastFMUsername, - discordID: dbUser.discordID, - }, - album: { name: album, artist: { name: artist } }, - }); - - const errors = this.parseErrors(response); - - if (errors) { - throw new UnknownMirrorballError(); - } - - if (!response.ratings.ratings.length) { - throw new CouldNotFindRatingError(); - } - - const { rating, rateYourMusicAlbum } = response.ratings.ratings[0]; - - const albumInfo = await this.lastFMService.albumInfo(this.ctx, { - artist, - album, - }); - - const albumCover = await this.albumCoverService.get( - this.ctx, - albumInfo.images.get("large"), - { - metadata: { artist, album }, - } - ); - - const embed = this.minimalEmbed() - .setHeader("RateYourMusic rating") - .setTitle( - `${rateYourMusicAlbum.artistName} - ${rateYourMusicAlbum.title}` - ) - .setDescription(`${displayRating(rating)}`) - .setThumbnail(albumCover || ""); - - await this.reply(embed); - } -} diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/Stats.ts b/src/commands/Lastfm/Mirrorball/RateYourMusic/Stats.ts deleted file mode 100644 index 343ef8ca..00000000 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/Stats.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { mean } from "mathjs"; -import { NoImportedRatingsFound } from "../../../../errors/commands/library"; -import { UnknownMirrorballError } from "../../../../errors/errors"; -import { toInt } from "../../../../helpers/lastfm/"; -import { extraWideSpace } from "../../../../helpers/specialCharacters"; -import { standardMentions } from "../../../../lib/context/arguments/mentionTypes/mentions"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { displayNumber, displayRating } from "../../../../lib/ui/displays"; -import { MirrorballRateYourMusicAlbum } from "../../../../services/mirrorball/MirrorballTypes"; -import { RateYourMusicIndexingChildCommand } from "./RateYourMusicChildCommand"; -import { StatsConnector, StatsParams, StatsResponse } from "./connectors"; - -const args = { - ...standardMentions, -} satisfies ArgumentsMap; - -interface Curve { - [rating: number]: number; -} - -export class Stats extends RateYourMusicIndexingChildCommand< - StatsResponse, - StatsParams, - typeof args -> { - connector = new StatsConnector(); - - idSeed = "shasha hakyung"; - description = "Shows what you've rated an artists albums"; - - arguments = args; - - slashCommand = true; - - async run() { - const { dbUser, discordUser } = await this.getMentions({ - fetchDiscordUser: true, - reverseLookup: { required: true }, - indexedRequired: true, - }); - - const perspective = this.usersService.discordPerspective( - this.author, - discordUser - ); - - const response = await this.query({ - user: { discordID: dbUser.discordID }, - }); - - const errors = this.parseErrors(response); - - if (errors) { - throw new UnknownMirrorballError(); - } - - if (!response.ratings.ratings.length) { - throw new NoImportedRatingsFound(this.prefix); - } - - const ratingsCounts = this.getRatingsCounts(response.ratings.ratings); - - const embed = this.minimalEmbed() - .setTitle(`${perspective.upper.possessive} RateYourMusic statistics`) - .setDescription( - `_${displayNumber( - response.ratings.ratings.length, - - "total rating" - )}, Average rating: ${displayNumber( - (mean(response.ratings.ratings.map((r) => r.rating)) / 2).toFixed(3) - )}_ - -${Object.entries(ratingsCounts) - .sort((a, b) => toInt(b) - toInt(a)) - .map( - ([rating, count]) => - `${displayRating(toInt(rating))}${extraWideSpace}${displayNumber(count)}` - ) - .join("\n")}` - ); - - await this.reply(embed); - } - - private getRatingsCounts( - ratings: { - rating: number; - rateYourMusicAlbum: MirrorballRateYourMusicAlbum; - }[] - ): Curve { - const curve = {} as Curve; - - for (const rating of ratings) { - curve[rating.rating] = ~~curve[rating.rating] + 1; - } - - return curve; - } -} diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/connectors.ts b/src/commands/Lastfm/Mirrorball/RateYourMusic/connectors.ts deleted file mode 100644 index 0d7feed3..00000000 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/connectors.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { gql } from "apollo-server-express"; -import { BaseConnector } from "../../../../lib/indexing/BaseConnector"; -import { - AlbumInput, - ArtistInput, - MirrorballPageInfo, - MirrorballRateYourMusicAlbum, - UserInput, -} from "../../../../services/mirrorball/MirrorballTypes"; - -// ImportRatings -export interface ImportRatingsResponse { - importRatings: {}; -} - -export interface ImportRatingsParams { - user: UserInput; - csv: string; -} - -export class ImportRatingsConnector extends BaseConnector< - ImportRatingsResponse, - ImportRatingsParams -> { - query = gql` - mutation importRatings($user: UserInput!, $csv: String!) { - importRatings(user: $user, csv: $csv) - } - `; -} - -// Rating -export interface RatingResponse { - ratings: { - ratings: [ - { - rating: number; - rateYourMusicAlbum: MirrorballRateYourMusicAlbum; - } - ]; - }; - pageInfo: MirrorballPageInfo; -} - -export interface RatingParams { - user: UserInput; - album: AlbumInput; -} - -export class RatingConnector extends BaseConnector< - RatingResponse, - RatingParams -> { - query = gql` - query rating($user: UserInput, $album: AlbumInput) { - ratings( - settings: { user: $user, album: $album, pageInput: { limit: 1 } } - ) { - ratings { - rating - rateYourMusicAlbum { - title - artistName - } - } - pageInfo { - recordCount - } - } - } - `; -} - -// ArtistRatings -export interface ArtistRatingsResponse { - ratings: { - ratings: { - rating: number; - rateYourMusicAlbum: MirrorballRateYourMusicAlbum; - }[]; - }; - artist?: { - artistName: string; - artistNativeName: string; - }; -} - -export interface ArtistRatingsParams { - user: UserInput; - artist: ArtistInput; - artistKeywords: string; -} - -export class ArtistRatingsConnector extends BaseConnector< - ArtistRatingsResponse, - ArtistRatingsParams -> { - query = gql` - query artistRatings( - $user: UserInput - $artist: ArtistInput - $artistKeywords: String! - ) { - ratings(settings: { user: $user, album: { artist: $artist } }) { - ratings { - rating - rateYourMusicAlbum { - title - artistName - releaseYear - rateYourMusicID - } - } - } - - artist: rateYourMusicArtist(keywords: $artistKeywords) { - artistName - artistNativeName - } - } - `; -} - -// Stats -export interface StatsResponse { - ratings: { - ratings: { - rating: number; - rateYourMusicAlbum: MirrorballRateYourMusicAlbum; - }[]; - }; -} - -export interface StatsParams { - user: UserInput; -} - -export class StatsConnector extends BaseConnector { - query = gql` - query stats($user: UserInput) { - ratings(settings: { user: $user }) { - ratings { - rating - rateYourMusicAlbum { - title - artistName - } - } - } - } - `; -} - -// Ratings -export interface RatingsResponse { - ratings: { - ratings: { - rating: number; - rateYourMusicAlbum: MirrorballRateYourMusicAlbum; - }[]; - pageInfo: MirrorballPageInfo; - }; -} - -export interface RatingsParams { - user: UserInput; - pageInput: { limit: number; offset: number }; - rating?: number; -} - -export class RatingsConnector extends BaseConnector< - RatingsResponse, - RatingsParams -> { - query = gql` - query stats($user: UserInput, $pageInput: PageInput, $rating: Int) { - ratings( - settings: { user: $user, pageInput: $pageInput, rating: $rating } - ) { - ratings { - rating - rateYourMusicAlbum { - title - artistName - } - } - pageInfo { - recordCount - } - } - } - `; -} - -// Ratings -export interface RatingsTasteResponse { - sender: { - ratings: { - rating: number; - rateYourMusicAlbum: MirrorballRateYourMusicAlbum; - }[]; - pageInfo: MirrorballPageInfo; - }; - mentioned: { - ratings: { - rating: number; - rateYourMusicAlbum: MirrorballRateYourMusicAlbum; - }[]; - pageInfo: MirrorballPageInfo; - }; -} - -export interface RatingsTasteParams { - sender: UserInput; - mentioned: UserInput; -} - -export class RatingsTasteConnector extends BaseConnector< - RatingsTasteResponse, - RatingsTasteParams -> { - query = gql` - query stats($sender: UserInput, $mentioned: UserInput) { - sender: ratings(settings: { user: $sender }) { - ratings { - rating - rateYourMusicAlbum { - rateYourMusicID - title - artistName - } - } - pageInfo { - recordCount - } - } - - mentioned: ratings(settings: { user: $mentioned }) { - ratings { - rating - rateYourMusicAlbum { - rateYourMusicID - title - artistName - } - } - pageInfo { - recordCount - } - } - } - `; -} diff --git a/src/commands/Lastfm/Mirrorball/WhoKnows/WhoFirst/WhoFirstArtist.ts b/src/commands/Lastfm/Mirrorball/WhoKnows/WhoFirst/WhoFirstArtist.ts deleted file mode 100644 index 3db6a02e..00000000 --- a/src/commands/Lastfm/Mirrorball/WhoKnows/WhoFirst/WhoFirstArtist.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { MirrorballError } from "../../../../../errors/errors"; -import { bold, italic } from "../../../../../helpers/discord"; -import { LastfmLinks } from "../../../../../helpers/lastfm/LastfmLinks"; -import { convertMirrorballDate } from "../../../../../helpers/mirrorball"; -import { Variation } from "../../../../../lib/command/Command"; -import { VARIATIONS } from "../../../../../lib/command/variations"; -import { - prefabArguments, - prefabFlags, -} from "../../../../../lib/context/arguments/prefabArguments"; -import { ArgumentsMap } from "../../../../../lib/context/arguments/types"; -import { Emoji } from "../../../../../lib/emoji/Emoji"; -import { LineConsolidator } from "../../../../../lib/LineConsolidator"; -import { - displayDate, - displayLink, - displayNumber, - displayNumberedList, -} from "../../../../../lib/ui/displays"; -import { WhoKnowsBaseCommand } from "../WhoKnowsBaseCommand"; -import { - WhoFirstArtistConnector, - WhoFirstArtistParams, - WhoFirstArtistResponse, -} from "./connectors"; - -const args = { - ...prefabArguments.artist, - noRedirect: prefabFlags.noRedirect, -} satisfies ArgumentsMap; - -export default class WhoFirstArtist extends WhoKnowsBaseCommand< - WhoFirstArtistResponse, - WhoFirstArtistParams, - typeof args -> { - connector = new WhoFirstArtistConnector(); - - idSeed = "shasha garam"; - aliases = ["wf", "whofirst", "wfa"]; - - subcategory = "whofirst"; - description = "See who first scrobbled an artist"; - - variations: Variation[] = [ - { - name: "wholast", - variation: ["wholastartist", "wl", "gwl", "wholast", "wla"], - description: "Shows who *last* scrobbled an artist", - }, - VARIATIONS.global("wf", "wl", "wla"), - ]; - - slashCommand = true; - - arguments = args; - - async run() { - const whoLast = this.variationWasUsed("wholast"); - - const { senderRequestable, senderUser, senderLilacUser, senderUsername } = - await this.getMentions({ - senderRequired: !this.parsedArguments.artist, - fetchLilacUser: true, - }); - - const artistName = await this.lastFMArguments.getArtist( - this.ctx, - senderRequestable, - { redirect: !this.parsedArguments.noRedirect } - ); - - const response = await this.query({ - whoLast, - artist: { name: artistName }, - settings: { - guildID: this.isGlobal() ? undefined : this.requiredGuild.id, - limit: 20, - }, - }); - - const errors = this.parseErrors(response); - - if (errors) { - throw new MirrorballError(errors.errors[0].message); - } - - await this.cacheUserInfo(response.whoFirstArtist.rows.map((u) => u.user)); - - const { rows, artist } = response.whoFirstArtist; - const { undated, senderUndated } = this.getUndated(response); - - const description = new LineConsolidator().addLines( - { - shouldDisplay: !!undated.length, - string: - italic( - `${displayNumber(undated.length, "user")} with undated scrobbles` - ) + "\n", - }, - { - shouldDisplay: senderUndated, - string: `\`${rows.length > 9 ? " " : ""}•\`. ${bold( - displayLink( - this.ctx.requiredAuthorMember.nickname || this.author.username, - LastfmLinks.libraryArtistPage(senderUsername, artist.name) - ) - )} - \`(undated)\``, - }, - { - shouldDisplay: artist && !!rows.length, - string: displayNumberedList( - rows.map( - (wk) => - `${this.displayUser(wk.user, { - customProfileLink: LastfmLinks.libraryArtistPage( - wk.user.username, - artist.name - ), - })} - ${this.displayScrobbleDate( - convertMirrorballDate(wk.scrobbledAt) - )}` - ) - ), - }, - { - shouldDisplay: !artist || rows.length === 0, - string: "No one has scrobbled this artist", - } - ); - - const embed = this.minimalEmbed() - .setTitle( - `${Emoji.usesIndexedDataTitle} Who ${ - whoLast ? "last" : "first" - } scrobbled ${bold(artist.name)}${this.isGlobal() ? " globally" : ""}?` - ) - .setDescription(description) - .setFooter(this.footerHelp(senderUser, senderLilacUser)); - - await this.reply(embed); - } - - private getUndated(response: WhoFirstArtistResponse): { - senderUndated: boolean; - undated: string[]; - } { - const filtered = response.whoFirstArtist.undated.filter( - (u) => u.user.discordID !== this.author.id - ); - - return { - undated: filtered.map((u) => u.user.discordID), - senderUndated: filtered.length !== response.whoFirstArtist.undated.length, - }; - } - - private displayScrobbleDate(date: Date) { - // Before February 13th, 2005 - const isPreDatedScrobbles = date.getTime() / 1000 < 1108368000; - - return displayDate(date) + (isPreDatedScrobbles ? " (or earlier)" : ""); - } -} diff --git a/src/commands/Lastfm/Mirrorball/WhoKnows/WhoFirst/connectors.ts b/src/commands/Lastfm/Mirrorball/WhoKnows/WhoFirst/connectors.ts deleted file mode 100644 index 12f9f2bb..00000000 --- a/src/commands/Lastfm/Mirrorball/WhoKnows/WhoFirst/connectors.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { gql } from "apollo-server-express"; -import { BaseConnector } from "../../../../../lib/indexing/BaseConnector"; -import { - ArtistInput, - MirrorballUser, - WhoKnowsSettings, -} from "../../../../../services/mirrorball/MirrorballTypes"; - -// WhoFirstArtist -export interface WhoFirstArtistResponse { - whoFirstArtist: { - artist: { - name: string; - }; - rows: { - user: MirrorballUser; - scrobbledAt: number; - }[]; - - undated: { - user: { discordID: string }; - }[]; - }; -} - -export interface WhoFirstArtistParams { - artist: ArtistInput; - settings?: WhoKnowsSettings; - whoLast?: boolean; -} - -export class WhoFirstArtistConnector extends BaseConnector< - WhoFirstArtistResponse, - WhoFirstArtistParams -> { - query = gql` - query whoFirstArtist( - $artist: ArtistInput! - $settings: WhoKnowsSettings - $whoLast: Boolean - ) { - whoFirstArtist(artist: $artist, settings: $settings, whoLast: $whoLast) { - artist { - name - } - - rows { - user { - username - discordID - privacy - } - scrobbledAt - } - - undated { - user { - discordID - } - } - } - } - `; -} diff --git a/src/commands/Lastfm/Mirrorball/WhoKnows/WhoKnowsBaseCommand.ts b/src/commands/Lastfm/Mirrorball/WhoKnows/WhoKnowsBaseCommand.ts deleted file mode 100644 index a980ad46..00000000 --- a/src/commands/Lastfm/Mirrorball/WhoKnows/WhoKnowsBaseCommand.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { User } from "../../../../database/entity/User"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { MirrorballBaseCommand } from "../../../../lib/indexing/MirrorballCommands"; -import { NicknameService } from "../../../../services/Discord/NicknameService"; -import { - DisplayUserOptions, - WhoKnowsService, -} from "../../../../services/Discord/WhoKnowsService"; -import { ServiceRegistry } from "../../../../services/ServicesRegistry"; -import { LilacPrivacy } from "../../../../services/lilac/LilacAPIService.types"; -import { LilacUser } from "../../../../services/lilac/converters/user"; -import { MirrorballUser } from "../../../../services/mirrorball/MirrorballTypes"; - -export abstract class WhoKnowsBaseCommand< - R, - P, - A extends ArgumentsMap = {} -> extends MirrorballBaseCommand { - nicknameService = ServiceRegistry.get(NicknameService); - whoKnowsService = ServiceRegistry.get(WhoKnowsService); - guildRequired = true; - - protected notIndexedHelp() { - return `Don't see yourself? Run ${this.prefix}index to download all your data!`; - } - protected unsetPrivacyHelp() { - return `Your privacy is currently unset! See ${this.prefix}privacy for more info`; - } - - protected isGlobal() { - return this.variationWasUsed("global"); - } - - protected footerHelp(senderUser?: User, senderLilacUser?: LilacUser) { - return !senderUser?.isIndexed - ? this.notIndexedHelp() - : this.isGlobal() && senderLilacUser?.privacy === LilacPrivacy.Unset - ? this.unsetPrivacyHelp() - : ""; - } - - protected displayUser( - user: MirrorballUser, - options?: Partial - ): string { - return this.whoKnowsService.displayUser(this.ctx, user, options); - } - - protected async cacheUserInfo(users: MirrorballUser[]) { - await this.nicknameService.cacheNicknames(this.ctx, users); - if (this.isGlobal()) { - await this.nicknameService.cacheUsernames(this.ctx, users); - } - } -} diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/ArtistRatings.ts b/src/commands/Lastfm/RateYourMusic/ArtistRatings.ts similarity index 57% rename from src/commands/Lastfm/Mirrorball/RateYourMusic/ArtistRatings.ts rename to src/commands/Lastfm/RateYourMusic/ArtistRatings.ts index e7150e6b..e9815812 100644 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/ArtistRatings.ts +++ b/src/commands/Lastfm/RateYourMusic/ArtistRatings.ts @@ -1,22 +1,16 @@ import { mean } from "mathjs"; -import { NoRatingsFromArtistError } from "../../../../errors/commands/library"; -import { UnknownMirrorballError } from "../../../../errors/errors"; -import { code, italic, sanitizeForDiscord } from "../../../../helpers/discord"; -import { emDash, extraWideSpace } from "../../../../helpers/specialCharacters"; -import { mostCommonOccurrence } from "../../../../helpers/stats"; -import { Flag } from "../../../../lib/context/arguments/argumentTypes/Flag"; -import { standardMentions } from "../../../../lib/context/arguments/mentionTypes/mentions"; -import { prefabArguments } from "../../../../lib/context/arguments/prefabArguments"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { displayNumber, displayRating } from "../../../../lib/ui/displays"; -import { ScrollingListView } from "../../../../lib/ui/views/ScrollingListView"; -import { MirrorballRating } from "../../../../services/mirrorball/MirrorballTypes"; -import { RateYourMusicIndexingChildCommand } from "./RateYourMusicChildCommand"; -import { - ArtistRatingsConnector, - ArtistRatingsParams, - ArtistRatingsResponse, -} from "./connectors"; +import { NoRatingsFromArtistError } from "../../../errors/commands/library"; +import { code, italic, sanitizeForDiscord } from "../../../helpers/discord"; +import { emDash, extraWideSpace } from "../../../helpers/specialCharacters"; +import { mostCommonOccurrence } from "../../../helpers/stats"; +import { Flag } from "../../../lib/context/arguments/argumentTypes/Flag"; +import { standardMentions } from "../../../lib/context/arguments/mentionTypes/mentions"; +import { prefabArguments } from "../../../lib/context/arguments/prefabArguments"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { displayNumber, displayRating } from "../../../lib/ui/displays"; +import { ScrollingListView } from "../../../lib/ui/views/ScrollingListView"; +import { LilacRating } from "../../../services/lilac/LilacAPIService.types"; +import { RateYourMusicChildCommand } from "./RateYourMusicChildCommand"; const args = { ...prefabArguments.artist, @@ -33,13 +27,7 @@ const args = { ...standardMentions, } satisfies ArgumentsMap; -export class ArtistRatings extends RateYourMusicIndexingChildCommand< - ArtistRatingsResponse, - ArtistRatingsParams, - typeof args -> { - connector = new ArtistRatingsConnector(); - +export class ArtistRatings extends RateYourMusicChildCommand { aliases = ["ara"]; idSeed = "sonamoo sumin"; description = "Shows your top rated albums from an artist"; @@ -52,7 +40,7 @@ export class ArtistRatings extends RateYourMusicIndexingChildCommand< const { senderRequestable, dbUser, discordUser } = await this.getMentions({ senderRequired: !this.parsedArguments.artist, fetchDiscordUser: true, - indexedRequired: true, + syncedRequired: true, }); const artist = await this.lastFMArguments.getArtist( @@ -65,27 +53,18 @@ export class ArtistRatings extends RateYourMusicIndexingChildCommand< discordUser ); - const response = await this.query({ - user: { - lastFMUsername: dbUser.lastFMUsername, - discordID: dbUser.discordID, - }, - artist: { name: artist }, - artistKeywords: artist, - }); - - const errors = this.parseErrors(response); + const [rymArtist, ratingsPage] = await Promise.all([ + this.lilacRatingsService.getArtist(this.ctx, artist), + this.lilacRatingsService.ratings(this.ctx, { + user: { discordID: dbUser.discordID }, + album: { artist: { name: artist } }, + }), + ]); const artistName = - response.artist?.artistName || - this.getArtistName(response.ratings.ratings) || - artist; - - if (errors) { - throw new UnknownMirrorballError(); - } + rymArtist.artistName || this.getArtistName(ratingsPage.ratings) || artist; - if (!response.ratings.ratings.length) { + if (!ratingsPage.ratings.length) { throw new NoRatingsFromArtistError(perspective); } @@ -100,22 +79,22 @@ export class ArtistRatings extends RateYourMusicIndexingChildCommand< ); const header = `**Average**: ${( - (mean(response.ratings.ratings.map((r) => r.rating)) as number) / 2 + (mean(ratingsPage.ratings.map((r) => r.rating)) as number) / 2 ).toFixed(2)}/5 from ${displayNumber( - response.ratings.ratings.length, + ratingsPage.ratings.length, "rating" )}`; const ratings = this.parsedArguments.yearly - ? response.ratings.ratings.sort( + ? ratingsPage.ratings.sort( (a, b) => b.rateYourMusicAlbum.releaseYear - a.rateYourMusicAlbum.releaseYear ) : this.parsedArguments.ids - ? response.ratings.ratings.sort((a, b) => + ? ratingsPage.ratings.sort((a, b) => a.rateYourMusicAlbum.title.localeCompare(b.rateYourMusicAlbum.title) ) - : response.ratings.ratings; + : ratingsPage.ratings; const simpleScrollingEmbed = new ScrollingListView(this.ctx, embed, { items: ratings, @@ -128,10 +107,7 @@ export class ArtistRatings extends RateYourMusicIndexingChildCommand< await this.reply(simpleScrollingEmbed); } - private generateTable( - ratings: MirrorballRating[], - artistName: string - ): string { + private generateTable(ratings: LilacRating[], artistName: string): string { return ratings .map((r, idx) => { return ( @@ -157,7 +133,7 @@ export class ArtistRatings extends RateYourMusicIndexingChildCommand< .join("\n"); } - private getArtistName(ratings: MirrorballRating[]): string { + private getArtistName(ratings: LilacRating[]): string { return mostCommonOccurrence( ratings.map((r) => r.rateYourMusicAlbum.artistName) )!; diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/Help.ts b/src/commands/Lastfm/RateYourMusic/Help.ts similarity index 91% rename from src/commands/Lastfm/Mirrorball/RateYourMusic/Help.ts rename to src/commands/Lastfm/RateYourMusic/Help.ts index 75cbd63f..8e81ec25 100644 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/Help.ts +++ b/src/commands/Lastfm/RateYourMusic/Help.ts @@ -1,4 +1,4 @@ -import { HelpEmbed } from "../../../../lib/ui/embeds/HelpEmbed"; +import { HelpEmbed } from "../../../lib/ui/embeds/HelpEmbed"; import { RateYourMusicChildCommand } from "./RateYourMusicChildCommand"; export class Help extends RateYourMusicChildCommand { diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/Import.ts b/src/commands/Lastfm/RateYourMusic/Import.ts similarity index 85% rename from src/commands/Lastfm/Mirrorball/RateYourMusic/Import.ts rename to src/commands/Lastfm/RateYourMusic/Import.ts index c2499f1b..1eec57e3 100644 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/Import.ts +++ b/src/commands/Lastfm/RateYourMusic/Import.ts @@ -5,15 +5,15 @@ import { TooManyAttachmentsError, UnknownRatingsImportError, WrongFileFormatAttachedError, -} from "../../../../errors/commands/library"; -import { CannotBeUsedAsASlashCommand } from "../../../../errors/errors"; -import { AlreadyImportingRatingsError } from "../../../../errors/external/rateYourMusic"; -import { StringArgument } from "../../../../lib/context/arguments/argumentTypes/StringArgument"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { Emoji } from "../../../../lib/emoji/Emoji"; -import { SuccessEmbed } from "../../../../lib/ui/embeds/SuccessEmbed"; -import { WarningEmbed } from "../../../../lib/ui/embeds/WarningEmbed"; -import { ConcurrentAction } from "../../../../services/ConcurrencyService"; +} from "../../../errors/commands/library"; +import { CannotBeUsedAsASlashCommand } from "../../../errors/errors"; +import { AlreadyImportingRatingsError } from "../../../errors/external/rateYourMusic"; +import { StringArgument } from "../../../lib/context/arguments/argumentTypes/StringArgument"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { Emoji } from "../../../lib/emoji/Emoji"; +import { SuccessEmbed } from "../../../lib/ui/embeds/SuccessEmbed"; +import { WarningEmbed } from "../../../lib/ui/embeds/WarningEmbed"; +import { ConcurrentAction } from "../../../services/ConcurrencyService"; import { RateYourMusicIndexingChildCommand } from "./RateYourMusicChildCommand"; import { ImportRatingsConnector, @@ -65,7 +65,7 @@ export class ImportRatings extends RateYourMusicIndexingChildCommand< await this.getMentions({ senderRequired: true, - indexedRequired: true, + syncedRequired: true, }); const ratings = await this.getRatings(); diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/Link.ts b/src/commands/Lastfm/RateYourMusic/Link.ts similarity index 86% rename from src/commands/Lastfm/Mirrorball/RateYourMusic/Link.ts rename to src/commands/Lastfm/RateYourMusic/Link.ts index c7f870f6..bd79cfc8 100644 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/Link.ts +++ b/src/commands/Lastfm/RateYourMusic/Link.ts @@ -1,6 +1,6 @@ -import { StringArgument } from "../../../../lib/context/arguments/argumentTypes/StringArgument"; -import { standardMentions } from "../../../../lib/context/arguments/mentionTypes/mentions"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; +import { StringArgument } from "../../../lib/context/arguments/argumentTypes/StringArgument"; +import { standardMentions } from "../../../lib/context/arguments/mentionTypes/mentions"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; import { RateYourMusicChildCommand } from "./RateYourMusicChildCommand"; const args = { @@ -17,6 +17,8 @@ export class Link extends RateYourMusicChildCommand { description = "Search Rateyourmusic for an album (or anything!)"; slashCommand = true; + aliases = ["ryms"]; + arguments = args; async run() { diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/RateYourMusicChildCommand.ts b/src/commands/Lastfm/RateYourMusic/RateYourMusicChildCommand.ts similarity index 53% rename from src/commands/Lastfm/Mirrorball/RateYourMusic/RateYourMusicChildCommand.ts rename to src/commands/Lastfm/RateYourMusic/RateYourMusicChildCommand.ts index 64844a88..48291fd8 100644 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/RateYourMusicChildCommand.ts +++ b/src/commands/Lastfm/RateYourMusic/RateYourMusicChildCommand.ts @@ -1,15 +1,17 @@ -import { BaseChildCommand } from "../../../../lib/command/ParentCommand"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { MirrorballChildCommand } from "../../../../lib/indexing/MirrorballCommands"; -import { LastFMArguments } from "../../../../services/LastFM/LastFMArguments"; -import { LastFMService } from "../../../../services/LastFM/LastFMService"; -import { ServiceRegistry } from "../../../../services/ServicesRegistry"; +import { BaseChildCommand } from "../../../lib/command/ParentCommand"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { MirrorballChildCommand } from "../../../lib/indexing/MirrorballCommands"; +import { LastFMArguments } from "../../../services/LastFM/LastFMArguments"; +import { LastFMService } from "../../../services/LastFM/LastFMService"; +import { ServiceRegistry } from "../../../services/ServicesRegistry"; +import { LilacRatingsService } from "../../../services/lilac/LilacRatingsService"; export abstract class RateYourMusicChildCommand< T extends ArgumentsMap = {} > extends BaseChildCommand { lastFMService = ServiceRegistry.get(LastFMService); lastFMArguments = ServiceRegistry.get(LastFMArguments); + lilacRatingsService = ServiceRegistry.get(LilacRatingsService); category = "lastfm"; parentName = "rateyourmusic"; diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/RateYourMusicParentCommand.ts b/src/commands/Lastfm/RateYourMusic/RateYourMusicParentCommand.ts similarity index 83% rename from src/commands/Lastfm/Mirrorball/RateYourMusic/RateYourMusicParentCommand.ts rename to src/commands/Lastfm/RateYourMusic/RateYourMusicParentCommand.ts index 6440b42e..ed2f052e 100644 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/RateYourMusicParentCommand.ts +++ b/src/commands/Lastfm/RateYourMusic/RateYourMusicParentCommand.ts @@ -1,5 +1,5 @@ -import { CommandGroup } from "../../../../lib/command/CommandGroup"; -import { ParentCommand } from "../../../../lib/command/ParentCommand"; +import { CommandGroup } from "../../../lib/command/CommandGroup"; +import { ParentCommand } from "../../../lib/command/ParentCommand"; import { ArtistRatings } from "./ArtistRatings"; import { Help } from "./Help"; import { ImportRatings } from "./Import"; @@ -34,9 +34,11 @@ export default class RateYourMusicParentCommand extends ParentCommand { // Taste "tasteratings", "ratingstaste", + // Link + "ryms", ]; - prefixes = ["rateyourmusic", "rym", "ryms"]; + prefixes = ["rateyourmusic", "rym"]; default = () => new Link(); children = new CommandGroup( diff --git a/src/commands/Lastfm/RateYourMusic/Rating.ts b/src/commands/Lastfm/RateYourMusic/Rating.ts new file mode 100644 index 00000000..26af4a79 --- /dev/null +++ b/src/commands/Lastfm/RateYourMusic/Rating.ts @@ -0,0 +1,68 @@ +import { CouldNotFindRatingError } from "../../../errors/commands/library"; +import { prefabArguments } from "../../../lib/context/arguments/prefabArguments"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { displayRating } from "../../../lib/ui/displays"; +import { AlbumCoverService } from "../../../services/moderation/AlbumCoverService"; +import { ServiceRegistry } from "../../../services/ServicesRegistry"; +import { RateYourMusicChildCommand } from "./RateYourMusicChildCommand"; + +const args = { + ...prefabArguments.album, +} satisfies ArgumentsMap; + +export class Rating extends RateYourMusicChildCommand { + idSeed = "sonamoo newsun"; + description = "Shows what you've rated an album"; + + arguments = args; + + slashCommand = true; + + albumCoverService = ServiceRegistry.get(AlbumCoverService); + + async run() { + const { senderRequestable, dbUser } = await this.getMentions({ + senderRequired: + !this.parsedArguments.artist || !this.parsedArguments.album, + reverseLookup: { required: true }, + syncedRequired: true, + }); + + const { artist, album } = await this.lastFMArguments.getAlbum( + this.ctx, + senderRequestable + ); + + const ratings = await this.lilacRatingsService.ratings(this.ctx, { + user: { discordID: dbUser.discordID }, + album: { name: album, artist: { name: artist } }, + pagination: { perPage: 1, page: 1 }, + }); + + if (!ratings.pagination.totalItems) { + throw new CouldNotFindRatingError(); + } + + const { rating, rateYourMusicAlbum } = ratings.ratings[0]; + + const albumInfo = await this.lastFMService.albumInfo(this.ctx, { + artist, + album, + }); + + const albumCover = await this.albumCoverService.get( + this.ctx, + albumInfo.images.get("large"), + { metadata: { artist, album } } + ); + + const embed = this.minimalEmbed() + .setTitle( + `${rateYourMusicAlbum.artistName} - ${rateYourMusicAlbum.title}` + ) + .setDescription(`Your rating: ${displayRating(rating)}`) + .setThumbnail(albumCover); + + await this.reply(embed); + } +} diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/Ratings.ts b/src/commands/Lastfm/RateYourMusic/Ratings.ts similarity index 52% rename from src/commands/Lastfm/Mirrorball/RateYourMusic/Ratings.ts rename to src/commands/Lastfm/RateYourMusic/Ratings.ts index e72725e9..e3276af1 100644 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/Ratings.ts +++ b/src/commands/Lastfm/RateYourMusic/Ratings.ts @@ -1,14 +1,14 @@ -import { UnknownMirrorballError } from "../../../../errors/errors"; -import { NoRatingsError } from "../../../../errors/external/rateYourMusic"; -import { StringArgument } from "../../../../lib/context/arguments/argumentTypes/StringArgument"; -import { standardMentions } from "../../../../lib/context/arguments/mentionTypes/mentions"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { PaginatedCache } from "../../../../lib/paginators/PaginatedCache"; -import { displayRating } from "../../../../lib/ui/displays"; -import { ScrollingView } from "../../../../lib/ui/views/ScrollingView"; -import { MirrorballRating } from "../../../../services/mirrorball/MirrorballTypes"; -import { RatingsConnector, RatingsParams, RatingsResponse } from "./connectors"; -import { RateYourMusicIndexingChildCommand } from "./RateYourMusicChildCommand"; +import { NoRatingsError } from "../../../errors/external/rateYourMusic"; +import { fourPerEmSpace } from "../../../helpers/specialCharacters"; +import { StringArgument } from "../../../lib/context/arguments/argumentTypes/StringArgument"; +import { standardMentions } from "../../../lib/context/arguments/mentionTypes/mentions"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { PaginatedCache } from "../../../lib/paginators/PaginatedCache"; +import { displayRating } from "../../../lib/ui/displays"; +import { ScrollingView } from "../../../lib/ui/views/ScrollingView"; +import { LilacRatingsFilters } from "../../../services/lilac/LilacAPIService.types"; +import { MirrorballRating } from "../../../services/mirrorball/MirrorballTypes"; +import { RateYourMusicChildCommand } from "./RateYourMusicChildCommand"; const args = { rating: new StringArgument({ @@ -19,15 +19,9 @@ const args = { ...standardMentions, } satisfies ArgumentsMap; -export class Ratings extends RateYourMusicIndexingChildCommand< - RatingsResponse, - RatingsParams, - typeof args -> { +export class Ratings extends RateYourMusicChildCommand { private readonly pageSize = 15; - connector = new RatingsConnector(); - aliases = ["rat"]; idSeed = "hot issue yewon"; description = @@ -48,7 +42,7 @@ export class Ratings extends RateYourMusicIndexingChildCommand< const { dbUser, discordUser } = await this.getMentions({ fetchDiscordUser: true, dbUserRequired: true, - indexedRequired: true, + syncedRequired: true, }); const perspective = this.usersService.discordPerspective( @@ -56,39 +50,32 @@ export class Ratings extends RateYourMusicIndexingChildCommand< discordUser ); - const initialPages = await this.query({ + const filters: LilacRatingsFilters = { rating, user: { - lastFMUsername: dbUser.lastFMUsername, discordID: dbUser.discordID, }, - pageInput: { limit: this.pageSize * 3, offset: 0 }, - }); - - const errors = this.parseErrors(initialPages); + }; - if (errors) { - throw new UnknownMirrorballError(); - } + const initialPages = await this.lilacRatingsService.ratings(this.ctx, { + ...filters, + pagination: { perPage: this.pageSize * 3, page: 1 }, + }); - if (!initialPages.ratings.pageInfo.recordCount) { + if (!initialPages.pagination.totalItems) { throw new NoRatingsError(this.prefix, rating, perspective); } const paginatedCache = new PaginatedCache(async (page) => { - const response = await this.query({ - user: { - lastFMUsername: dbUser.lastFMUsername, - discordID: dbUser.discordID, - }, - rating, - pageInput: { limit: this.pageSize, offset: this.pageSize * (page - 1) }, + const response = await this.lilacRatingsService.ratings(this.ctx, { + ...filters, + pagination: { perPage: this.pageSize, page }, }); - return response.ratings.ratings; + return response.ratings; }); - paginatedCache.cacheInitial(initialPages.ratings.ratings, this.pageSize); + paginatedCache.cacheInitial(initialPages.ratings, this.pageSize); const embed = this.minimalEmbed().setTitle( rating @@ -98,10 +85,8 @@ export class Ratings extends RateYourMusicIndexingChildCommand< const scrollingEmbed = new ScrollingView(this.ctx, embed, { initialItems: this.generateTable(await paginatedCache.getPage(1)), - totalPages: Math.ceil( - initialPages.ratings.pageInfo.recordCount / this.pageSize - ), - totalItems: initialPages.ratings.pageInfo.recordCount, + totalPages: Math.ceil(initialPages.pagination.totalItems / this.pageSize), + totalItems: initialPages.pagination.totalItems, itemName: "rating", }); @@ -130,8 +115,7 @@ export class Ratings extends RateYourMusicIndexingChildCommand< return ( (header ? header + "\n" : "") + - // this is a special space to make things align better - ` ${r.rateYourMusicAlbum.artistName} - ${r.rateYourMusicAlbum.title}` + `${fourPerEmSpace}${r.rateYourMusicAlbum.artistName} - ${r.rateYourMusicAlbum.title}` ); }) .join("\n") diff --git a/src/commands/Lastfm/RateYourMusic/Stats.ts b/src/commands/Lastfm/RateYourMusic/Stats.ts new file mode 100644 index 00000000..60e824c1 --- /dev/null +++ b/src/commands/Lastfm/RateYourMusic/Stats.ts @@ -0,0 +1,81 @@ +import { mean } from "mathjs"; +import { NoImportedRatingsFound } from "../../../errors/commands/library"; +import { toInt } from "../../../helpers/lastfm"; +import { extraWideSpace } from "../../../helpers/specialCharacters"; +import { standardMentions } from "../../../lib/context/arguments/mentionTypes/mentions"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { displayNumber, displayRating } from "../../../lib/ui/displays"; +import { LilacRating } from "../../../services/lilac/LilacAPIService.types"; +import { RateYourMusicChildCommand } from "./RateYourMusicChildCommand"; + +const args = { + ...standardMentions, +} satisfies ArgumentsMap; + +interface Curve { + [rating: number]: number; +} + +export class Stats extends RateYourMusicChildCommand { + idSeed = "shasha hakyung"; + description = "Shows your RateYourMusic statistics"; + + arguments = args; + + slashCommand = true; + + async run() { + const { dbUser, discordUser } = await this.getMentions({ + fetchDiscordUser: true, + reverseLookup: { required: true }, + syncedRequired: true, + }); + + const perspective = this.usersService.discordPerspective( + this.author, + discordUser + ); + + const ratings = await this.lilacRatingsService.ratings(this.ctx, { + user: { discordID: dbUser.discordID }, + }); + + if (!ratings.ratings.length) { + throw new NoImportedRatingsFound(this.prefix); + } + + const ratingsCounts = this.getRatingsCounts(ratings.ratings); + + const embed = this.minimalEmbed() + .setTitle(`${perspective.upper.possessive} RateYourMusic statistics`) + .setDescription( + `_${displayNumber( + ratings.ratings.length, + + "total rating" + )}, Average rating: ${displayNumber( + (mean(ratings.ratings.map((r) => r.rating)) / 2).toFixed(3) + )}_ + +${Object.entries(ratingsCounts) + .sort((a, b) => toInt(b) - toInt(a)) + .map( + ([rating, count]) => + `${displayRating(toInt(rating))}${extraWideSpace}${displayNumber(count)}` + ) + .join("\n")}` + ); + + await this.reply(embed); + } + + private getRatingsCounts(ratings: LilacRating[]): Curve { + const curve = {} as Curve; + + for (const rating of ratings) { + curve[rating.rating] = ~~curve[rating.rating] + 1; + } + + return curve; + } +} diff --git a/src/commands/Lastfm/Mirrorball/RateYourMusic/Taste.ts b/src/commands/Lastfm/RateYourMusic/Taste.ts similarity index 62% rename from src/commands/Lastfm/Mirrorball/RateYourMusic/Taste.ts rename to src/commands/Lastfm/RateYourMusic/Taste.ts index 3272fd25..b51e8467 100644 --- a/src/commands/Lastfm/Mirrorball/RateYourMusic/Taste.ts +++ b/src/commands/Lastfm/RateYourMusic/Taste.ts @@ -2,29 +2,23 @@ import { MentionedUserHasNoRatingsError, NoSharedRatingsError, NoUserToCompareRatingsToError, -} from "../../../../errors/commands/library"; -import { NoRatingsError } from "../../../../errors/external/rateYourMusic"; +} from "../../../errors/commands/library"; +import { NoRatingsError } from "../../../errors/external/rateYourMusic"; import { bold, - italic, mentionGuildMember, sanitizeForDiscord, -} from "../../../../helpers/discord"; -import { emDash } from "../../../../helpers/specialCharacters"; -import { Flag } from "../../../../lib/context/arguments/argumentTypes/Flag"; -import { standardMentions } from "../../../../lib/context/arguments/mentionTypes/mentions"; -import { ArgumentsMap } from "../../../../lib/context/arguments/types"; -import { displayNumber, displayRating } from "../../../../lib/ui/displays"; -import { ScrollingListView } from "../../../../lib/ui/views/ScrollingListView"; -import { ServiceRegistry } from "../../../../services/ServicesRegistry"; -import { TasteService } from "../../../../services/taste/TasteService"; -import { RatingPair } from "../../../../services/taste/TasteService.types"; -import { - RatingsTasteConnector, - RatingsTasteParams, - RatingsTasteResponse, -} from "./connectors"; -import { RateYourMusicIndexingChildCommand } from "./RateYourMusicChildCommand"; +} from "../../../helpers/discord"; +import { emDash } from "../../../helpers/specialCharacters"; +import { Flag } from "../../../lib/context/arguments/argumentTypes/Flag"; +import { standardMentions } from "../../../lib/context/arguments/mentionTypes/mentions"; +import { ArgumentsMap } from "../../../lib/context/arguments/types"; +import { displayNumber, displayRating } from "../../../lib/ui/displays"; +import { ScrollingListView } from "../../../lib/ui/views/ScrollingListView"; +import { ServiceRegistry } from "../../../services/ServicesRegistry"; +import { TasteService } from "../../../services/taste/TasteService"; +import { RatingPair } from "../../../services/taste/TasteService.types"; +import { RateYourMusicChildCommand } from "./RateYourMusicChildCommand"; const args = { ...standardMentions, @@ -41,13 +35,7 @@ const args = { }), } satisfies ArgumentsMap; -export class Taste extends RateYourMusicIndexingChildCommand< - RatingsTasteResponse, - RatingsTasteParams, - typeof args -> { - connector = new RatingsTasteConnector(); - +export class Taste extends RateYourMusicChildCommand { aliases = ["t", "tasteratings", "ratingstaste"]; idSeed = "dreamnote sinae"; description = "Shows the overlap between your ratings and another user's"; @@ -68,9 +56,9 @@ export class Taste extends RateYourMusicIndexingChildCommand< throw new NoUserToCompareRatingsToError(); } - const ratings = await this.query({ - mentioned: { discordID: discordUser.id }, + const ratings = await this.lilacRatingsService.ratingsTaste(this.ctx, { sender: { discordID: this.author.id }, + mentioned: { discordID: discordUser.id }, }); if (!ratings.sender.ratings?.length) { @@ -89,12 +77,12 @@ export class Taste extends RateYourMusicIndexingChildCommand< throw new NoSharedRatingsError(discordUser.id); } - const embedDescription = `Taste comparison for ${mentionGuildMember( + const embedDescription = `**Taste comparison for ${mentionGuildMember( this.author.id - )} and ${mentionGuildMember(discordUser.id)}\n\nComparing ${displayNumber( - ratings.sender.pageInfo.recordCount + )} and ${mentionGuildMember(discordUser.id)}**\n\nComparing ${displayNumber( + ratings.sender.pagination.totalItems )} and ${displayNumber( - ratings.mentioned.pageInfo.recordCount, + ratings.mentioned.pagination.totalItems, "rating" )}, ${displayNumber(tasteMatch.ratings.length, "similar rating")}\n_${ tasteMatch.percent @@ -107,9 +95,7 @@ export class Taste extends RateYourMusicIndexingChildCommand< items: tasteMatch.ratings, pageSize: 10, pageRenderer: (ratings) => { - return ( - italic(embedDescription) + "\n\n" + this.generateTable(ratings) - ); + return embedDescription + "\n\n" + this.generateTable(ratings); }, overrides: { itemName: "rating" }, } diff --git a/src/commands/Lastfm/RateYourMusic/connectors.ts b/src/commands/Lastfm/RateYourMusic/connectors.ts new file mode 100644 index 00000000..ad0a856e --- /dev/null +++ b/src/commands/Lastfm/RateYourMusic/connectors.ts @@ -0,0 +1,24 @@ +import { gql } from "apollo-server-express"; +import { BaseConnector } from "../../../lib/indexing/BaseConnector"; +import { UserInput } from "../../../services/mirrorball/MirrorballTypes"; + +// ImportRatings +export interface ImportRatingsResponse { + importRatings: {}; +} + +export interface ImportRatingsParams { + user: UserInput; + csv: string; +} + +export class ImportRatingsConnector extends BaseConnector< + ImportRatingsResponse, + ImportRatingsParams +> { + query = gql` + mutation importRatings($user: UserInput!, $csv: String!) { + importRatings(user: $user, csv: $csv) + } + `; +} diff --git a/src/commands/Lastfm/Reports/Year/Year.ts b/src/commands/Lastfm/Reports/Year/Year.ts index 2c1f86e4..27a7d715 100644 --- a/src/commands/Lastfm/Reports/Year/Year.ts +++ b/src/commands/Lastfm/Reports/Year/Year.ts @@ -44,7 +44,7 @@ export default class Year extends MirrorballBaseCommand< const { dbUser, discordUser } = await this.getMentions({ fetchDiscordUser: true, reverseLookup: { required: true }, - indexedRequired: true, + syncedRequired: true, }); const perspective = this.usersService.discordPerspective( diff --git a/src/commands/Lastfm/WhoKnows/LilacWhoKnowsBaseCommand.ts b/src/commands/Lastfm/WhoKnows/LilacWhoKnowsBaseCommand.ts index d749b542..1d30e2cb 100644 --- a/src/commands/Lastfm/WhoKnows/LilacWhoKnowsBaseCommand.ts +++ b/src/commands/Lastfm/WhoKnows/LilacWhoKnowsBaseCommand.ts @@ -8,7 +8,6 @@ import { import { LilacUser } from "../../../services/lilac/converters/user"; import { LilacPrivacy } from "../../../services/lilac/LilacAPIService.types"; import { LilacWhoKnowsService } from "../../../services/lilac/LilacWhoKnowsService"; -import { MirrorballUser } from "../../../services/mirrorball/MirrorballTypes"; import { ServiceRegistry } from "../../../services/ServicesRegistry"; import { client } from "../../../setup"; @@ -26,8 +25,8 @@ export abstract class WhoKnowsBaseCommand< whoKnowsService = ServiceRegistry.get(WhoKnowsService); lilacWhoKnowsService = ServiceRegistry.get(LilacWhoKnowsService); - protected notIndexedHelp() { - return `Don't see yourself? Run ${this.prefix}index to download all your data!`; + protected notSyncedHelp() { + return `Don't see yourself? Run ${this.prefix}sync to download all your data!`; } protected unsetPrivacyHelp() { return `Your privacy is currently unset! See ${this.prefix}privacy for more info`; @@ -37,16 +36,16 @@ export abstract class WhoKnowsBaseCommand< return this.variationWasUsed("global"); } - protected footerHelp(senderLilacUser?: LilacUser) { - return !senderLilacUser?.lastUpdated - ? this.notIndexedHelp() + protected footerHelp(senderLilacUser: LilacUser | undefined) { + return !senderLilacUser?.lastSynced + ? this.notSyncedHelp() : this.isGlobal() && senderLilacUser?.privacy === LilacPrivacy.Unset ? this.unsetPrivacyHelp() : ""; } protected displayUser( - user: MirrorballUser, + user: LilacUser, options?: Partial ): string { return this.whoKnowsService.displayUser(this.ctx, user, options); diff --git a/src/commands/Lastfm/WhoKnows/WhoFirst/WhoFirstArtist.ts b/src/commands/Lastfm/WhoKnows/WhoFirst/WhoFirstArtist.ts new file mode 100644 index 00000000..dd100a9a --- /dev/null +++ b/src/commands/Lastfm/WhoKnows/WhoFirst/WhoFirstArtist.ts @@ -0,0 +1,145 @@ +import { bold } from "../../../../helpers/discord"; +import { LastfmLinks } from "../../../../helpers/lastfm/LastfmLinks"; +import { convertLilacDate } from "../../../../helpers/lilac"; +import { LineConsolidator } from "../../../../lib/LineConsolidator"; +import { Variation } from "../../../../lib/command/Command"; +import { VARIATIONS } from "../../../../lib/command/variations"; +import { + prefabArguments, + prefabFlags, +} from "../../../../lib/context/arguments/prefabArguments"; +import { ArgumentsMap } from "../../../../lib/context/arguments/types"; +import { Emoji } from "../../../../lib/emoji/Emoji"; +import { + displayDate, + displayLink, + displayNumber, + displayNumberedList, +} from "../../../../lib/ui/displays"; +import { WhoKnowsBaseCommand } from "../LilacWhoKnowsBaseCommand"; + +const args = { + ...prefabArguments.artist, + noRedirect: prefabFlags.noRedirect, +} satisfies ArgumentsMap; + +export default class WhoFirstArtist extends WhoKnowsBaseCommand { + idSeed = "shasha garam"; + aliases = ["wf", "whofirst", "wfa"]; + + subcategory = "whofirst"; + description = "See who first scrobbled an artist"; + + variations: Variation[] = [ + { + name: "wholast", + variation: ["wholastartist", "wl", "gwl", "wholast", "wla"], + description: "Shows who *last* scrobbled an artist", + }, + VARIATIONS.global("wf", "wl", "wla"), + ]; + + slashCommand = true; + + arguments = args; + + async run() { + const whoLast = this.variationWasUsed("wholast"); + + const { senderRequestable, senderLilacUser, senderUser } = + await this.getMentions({ + senderRequired: !this.parsedArguments.artist, + fetchLilacUser: true, + }); + + const artistName = await this.lastFMArguments.getArtist( + this.ctx, + senderRequestable, + { redirect: !this.parsedArguments.noRedirect } + ); + + const guildID = this.isGlobal() ? undefined : this.requiredGuild.id; + + const { whoFirstArtist: whoFirst, whoFirstArtistRank: whoFirstRank } = + await this.lilacWhoKnowsService.whoFirstArtist( + this.ctx, + artistName, + this.author.id, + guildID, + 15, + whoLast + ); + + await this.cacheUserInfo(whoFirst.rows.map((u) => u.user)); + + const { rows, artist } = whoFirst; + const { rank, totalListeners } = whoFirstRank; + + const userListenedDate = whoLast + ? whoFirstRank.lastScrobbled + : whoFirstRank.firstScrobbled; + + const description = new LineConsolidator().addLines( + { + shouldDisplay: artist && !!rows.length, + string: displayNumberedList( + rows.map( + (wf) => + `${this.displayUser(wf.user, { + customProfileLink: LastfmLinks.libraryArtistPage( + wf.user.username, + artist.name + ), + })} - ${this.displayScrobbleDate( + convertLilacDate(whoLast ? wf.lastScrobbled : wf.firstScrobbled) + )}` + ) + ), + }, + { + shouldDisplay: rank > 15, + string: + `\n\`${rank}\` ` + + bold( + displayLink( + this.payload.member?.nickname || this.payload.author.username, + LastfmLinks.userPage(senderUser?.lastFMUsername!) + ) + ) + + `- **${this.displayScrobbleDate(convertLilacDate(userListenedDate))}`, + }, + { + shouldDisplay: !artist || rows.length === 0, + string: "No one has scrobbled this artist", + } + ); + + const embed = this.minimalEmbed() + .setTitle( + `${Emoji.usesIndexedDataTitle} Who ${ + whoLast ? "last" : "first" + } scrobbled ${bold(artist.name)}${this.isGlobal() ? " globally" : ""}?` + ) + .setDescription(description) + .setFooter( + ( + `${displayNumber( + totalListeners, + this.isGlobal() ? "global listener" : "server listener" + )}\n` + this.footerHelp(senderLilacUser) + ).trim() + ) + .setFooterIcon( + this.isGlobal() ? this.gowonIconURL : this.guild?.iconURL() ?? undefined + ); + + await this.reply(embed); + } + + private displayScrobbleDate(date: Date) { + // Before February 13th, 2005 + const isPreDatedScrobbles = date.getTime() / 1000 < 1108368000; + + return displayDate(date) + (isPreDatedScrobbles ? " (or earlier)" : ""); + } +} diff --git a/src/commands/Lastfm/WhoKnows/WhoKnowsArtist.ts b/src/commands/Lastfm/WhoKnows/WhoKnowsArtist.ts index 529ee2eb..a756aee7 100644 --- a/src/commands/Lastfm/WhoKnows/WhoKnowsArtist.ts +++ b/src/commands/Lastfm/WhoKnows/WhoKnowsArtist.ts @@ -38,13 +38,11 @@ export default class WhoKnowsArtist extends WhoKnowsBaseCommand { crownsService = ServiceRegistry.get(CrownsService); async run() { - const { senderRequestable, senderUser } = await this.getMentions({ - senderRequired: !this.parsedArguments.artist, - }); - - const senderLilacUser = await this.lilacUsersService.fetch(this.ctx, { - discordID: this.author.id, - }); + const { senderRequestable, senderUser, senderLilacUser } = + await this.getMentions({ + senderRequired: !this.parsedArguments.artist, + fetchLilacUser: true, + }); const artistName = await this.lastFMArguments.getArtist( this.ctx, diff --git a/src/commands/Meta/Patreon.ts b/src/commands/Meta/Patreon.ts index 52e4cbcd..a1807c06 100644 --- a/src/commands/Meta/Patreon.ts +++ b/src/commands/Meta/Patreon.ts @@ -6,12 +6,17 @@ export default class Patreon extends Command { subcategory = "about"; description = "Displays the link to sign up for Patreon"; - aliases = ["pat", "donate"]; + aliases = ["donate", "backer"]; async run() { const embed = this.minimalEmbed().setDescription( `${Emoji.gowonPatreon} You can support me at: https://www.patreon.com/gowon_ -Anything is appreciated!` +Anything is appreciated! + +This unlocks backer features on the bot, such as: +- Full scrobbles indexing +- Custom album art +- More to come!` ); await this.reply(embed); diff --git a/src/commands/Meta/SetPatron.ts b/src/commands/Meta/SetPatron.ts index edbc8ce7..ef13ca58 100644 --- a/src/commands/Meta/SetPatron.ts +++ b/src/commands/Meta/SetPatron.ts @@ -16,15 +16,15 @@ export default class SetPatron extends Command { idSeed = "hello venus yooyoung"; subcategory = "developer"; - description = "Sets a user as a patron"; - aliases = ["setp"]; + description = "Sets a user as a backer"; + aliases = ["setp", "setbacker"]; secretCommand = true; devCommand = true; variations: Variation[] = [ { name: "unset", - variation: ["unsetpatron", "unsetp"], + variation: ["unsetpatron", "unsetp", "unsetbacker"], }, ]; @@ -42,7 +42,7 @@ export default class SetPatron extends Command { const id = this.parsedArguments.user?.id || this.parsedArguments.userID; try { - await this.usersService.setPatron( + await this.usersService.setAsBacker( this.ctx, id!, !this.variationWasUsed("unset") @@ -54,7 +54,7 @@ export default class SetPatron extends Command { const embed = new SuccessEmbed().setDescription( `Successfully ${ this.variationWasUsed("unset") ? "un" : "" - }set ${id} as a patron!` + }set ${id} as a backer!` ); await this.reply(embed); diff --git a/src/commands/Meta/UserInfo.ts b/src/commands/Meta/UserInfo.ts index 15618d9b..5920d923 100644 --- a/src/commands/Meta/UserInfo.ts +++ b/src/commands/Meta/UserInfo.ts @@ -57,8 +57,8 @@ export default class UserInfo extends Command { shouldDisplay: !!dbUser.roles?.length, }, { - string: `${Emoji.gowonPatreon} Patron!`, - shouldDisplay: dbUser.isPatron, + string: `${Emoji.gowonPatreon} Backer!`, + shouldDisplay: dbUser.hasPremium, }, { string: `${Emoji.checkmark} Authenticated!`, @@ -75,7 +75,7 @@ export default class UserInfo extends Command { `**Cached scrobbles**: ${displayNumber(cachedPlaycount)}`, `**Commands run**: ${displayNumber(commandRunCount)}`, `**Last updated**: ${ - lilacUserInfo?.lastUpdated ? ago(lilacUserInfo.lastUpdated) : "(never)" + lilacUserInfo?.lastSynced ? ago(lilacUserInfo.lastSynced) : "(never)" }`, { shouldDisplay: topCommands.length > 0, diff --git a/src/database/entity/User.ts b/src/database/entity/User.ts index efec17e1..9aceab5d 100644 --- a/src/database/entity/User.ts +++ b/src/database/entity/User.ts @@ -71,6 +71,10 @@ export class User extends BaseEntity { @Column("simple-array", { nullable: true }) roles?: CommandAccessRoleName[]; + get hasPremium(): boolean { + return this.isPatron; + } + static async toDiscordUser( guild: Guild, discordID: string diff --git a/src/errors/commands/library.ts b/src/errors/commands/library.ts index 0375622b..547bbf17 100644 --- a/src/errors/commands/library.ts +++ b/src/errors/commands/library.ts @@ -2,7 +2,7 @@ import { bold, italic, mentionGuildMember } from "../../helpers/discord"; import { Perspective } from "../../lib/Perspective"; import { ClientError } from "../errors"; -export class NoScrobblesOfAlbumError extends ClientError { +export class NoScrobblesOfAnyAlbumsFromArtistError extends ClientError { constructor(perspective: Perspective, artistName: string) { super( `${perspective.plusToHave} no scrobbles of any albums from ${bold( @@ -12,7 +12,7 @@ export class NoScrobblesOfAlbumError extends ClientError { } } -export class NoScrobblesForTrackError extends ClientError { +export class NoScrobblesOfTrackError extends ClientError { constructor(perspective: Perspective, artistName: string, trackName: string) { super( `${perspective.plusToHave} no scrobbles for ${italic( @@ -46,6 +46,16 @@ export class NoScrobblesOfArtistError extends ClientError { } } +export class NoScrobblesOfAlbumError extends ClientError { + constructor(perspective: Perspective, artistName: string, albumName: string) { + super( + `${perspective.plusToHave} no scrobbles of any songs from ${italic( + albumName + )} by ${bold(artistName)}!` + ); + } +} + export class NoRatingsFromArtistError extends ClientError { constructor(perspective: Perspective) { super( @@ -90,14 +100,14 @@ export class NoRatingsFileAttatchedError extends ClientError { export class CouldNotFindRatingError extends ClientError { constructor() { - super("Couldn't find this album in your ratings!"); + super("Couldn't find that album in your ratings!"); } } export class NoImportedRatingsFound extends ClientError { constructor(prefix: string) { super( - `You don't have any ratings imported yet! To import your ratings see \`${prefix}ryms help\`` + `You don't have any ratings imported yet! To import your ratings see \`${prefix}rym help\`` ); } } diff --git a/src/errors/ui.ts b/src/errors/ui.ts index 53a0dfa7..7449f2d4 100644 --- a/src/errors/ui.ts +++ b/src/errors/ui.ts @@ -1,23 +1,23 @@ export class CannotSwitchToTabError extends Error { - name = "CannotSwitchToTabError"; - constructor() { super(`Couldn't find a tab to switch to!`); } } export class NoMessageToReactToError extends Error { - name = "NoMessageToReactTo"; - constructor() { super(`There is no message to react to!`); } } export class ViewHasNotBeenSentError extends Error { - name = "ViewHasNotBeenSentError"; - constructor() { super(`This view has not been sent yet!`); } } + +export class ViewCannotBeSentError extends Error { + constructor() { + super("This view cannot be sent!"); + } +} diff --git a/src/errors/user.ts b/src/errors/user.ts index 16868cf9..d8910f3b 100644 --- a/src/errors/user.ts +++ b/src/errors/user.ts @@ -98,3 +98,23 @@ export class CouldNotFindUserWithUsername extends ClientError { ); } } + +export class CommandRequiresBackerError extends ClientError { + constructor(prefix: string) { + super( + `This command is only available to backers! See ${code( + `${prefix}backer` + )} for more info` + ); + } +} + +export class CommandRequiresResyncError extends ClientError { + constructor(prefix: string) { + super( + `This command requires updated sync data! Run \`${code( + `${prefix}sync` + )}\` to re-sync your account.` + ); + } +} diff --git a/src/helpers/specialCharacters.ts b/src/helpers/specialCharacters.ts index 5e470d3d..2fd7dcae 100644 --- a/src/helpers/specialCharacters.ts +++ b/src/helpers/specialCharacters.ts @@ -3,6 +3,7 @@ export const emDash = "—"; export const extraWideSpace = " "; export const openQuote = "“"; export const closeQuote = "”"; +export const fourPerEmSpace = " "; export function quote(string: string): string { return openQuote + string + closeQuote; diff --git a/src/lib/Lilac/LilacBaseCommand.ts b/src/lib/Lilac/LilacBaseCommand.ts index d8bd370c..cb715d57 100644 --- a/src/lib/Lilac/LilacBaseCommand.ts +++ b/src/lib/Lilac/LilacBaseCommand.ts @@ -7,7 +7,7 @@ import { ArgumentsMap } from "../context/arguments/types"; export abstract class LilacBaseCommand< T extends ArgumentsMap = {} > extends Command { - protected readonly progressBarWidth = 15; + public static readonly progressBarWidth = 15; // The old Mirrorball commands had a connector field // that shouldn't be present on the new Lilac commands @@ -16,6 +16,6 @@ export abstract class LilacBaseCommand< lastFMService = ServiceRegistry.get(LastFMService); lastFMArguments = ServiceRegistry.get(LastFMArguments); - readonly indexingHelp = - '"Indexing" means downloading all your last.fm data. This is required for many commands to function.'; + readonly syncHelp = + '"Syncing" means downloading all your Last.fm data. This is required for many commands to function.'; } diff --git a/src/lib/command/Command.ts b/src/lib/command/Command.ts index f27d7190..f7311981 100644 --- a/src/lib/command/Command.ts +++ b/src/lib/command/Command.ts @@ -515,7 +515,7 @@ export abstract class Command { if ( this.author.id && Chance().bool({ likelihood: 33 }) && - !["update", "index", "login", "logout"].includes(this.name) && + !["update", "sync", "login", "logout"].includes(this.name) && !this.gowonClient.isInIssueMode && !this.gowonClient.isTesting ) { diff --git a/src/lib/emoji/icons.ts b/src/lib/emoji/icons.ts index a06b0294..5a978002 100644 --- a/src/lib/emoji/icons.ts +++ b/src/lib/emoji/icons.ts @@ -31,6 +31,11 @@ export const icons = { arrowRight: "<:next:1197111479883812895>", arrowLast: "<:last:1197111411055263764>", + // Progress bar icons + remainingProgress: "<:loading:1197111489337761832>", + progress: "<:progress:1197111521109618758>", + moreProgress: "<:moreProgress:1197111523064164362>", + // Native emoji overrides fire: "<:fire:1197111444542595162>", chart: "<:chart:1197111406793850900>", diff --git a/src/lib/nowplaying/DatasourceService.ts b/src/lib/nowplaying/DatasourceService.ts index 1fd7e4ba..42666e6a 100644 --- a/src/lib/nowplaying/DatasourceService.ts +++ b/src/lib/nowplaying/DatasourceService.ts @@ -19,14 +19,28 @@ import { CardsService } from "../../services/dbservices/CardsService"; import { CrownsService } from "../../services/dbservices/crowns/CrownsService"; import { CrownDisplay } from "../../services/dbservices/crowns/CrownsService.types"; import { FishyService } from "../../services/fishy/FishyService"; -import { MirrorballService } from "../../services/mirrorball/MirrorballService"; +import { LilacAPIService } from "../../services/lilac/LilacAPIService"; +import { + LilacAlbumCountFilters, + LilacAlbumCountsPage, + LilacArtistCountFilters, + LilacArtistCountsPage, + LilacRatingsFilters, + LilacRatingsPage, + LilacUserInput, +} from "../../services/lilac/LilacAPIService.types"; import { UserInput } from "../../services/mirrorball/MirrorballTypes"; import { Logger } from "../Logger"; import { GowonContext } from "../context/Context"; import { Payload } from "../context/Payload"; import { TagConsolidator } from "../tags/TagConsolidator"; import { NowPlayingDependency } from "./base/BaseNowPlayingComponent"; -import { QueryPart, buildQuery, isQueryPart } from "./buildQuery"; +import { + QueryPart, + QueryPartName, + buildQuery, + isQueryPart, +} from "./buildQuery"; export interface ResolvedDependencies { [dependency: string]: any; @@ -56,8 +70,8 @@ export class DatasourceService extends BaseService { get lastFMService() { return ServiceRegistry.get(LastFMService); } - get mirrorballService() { - return ServiceRegistry.get(MirrorballService); + get lilacAPIService() { + return ServiceRegistry.get(LilacAPIService); } get crownsService() { return ServiceRegistry.get(CrownsService); @@ -210,41 +224,55 @@ export class DatasourceService extends BaseService { ); } - albumPlays(ctx: DatasourceServiceContext): QueryPart { - const user: UserInput = { - discordID: ctx.constants.resources!.dbUser.discordID, + artistCount(ctx: DatasourceServiceContext): QueryPart { + const acFilters: LilacArtistCountFilters = { + artists: [{ name: this.nowPlaying(ctx).artist }], + users: [this.userInput(ctx)], }; - const lpSettings = { + + return { + query: QueryPartName.ArtistCount, + variables: { acFilters }, + transformer(data: LilacArtistCountsPage) { + return data.artistCounts[0]; + }, + }; + } + + albumCount(ctx: DatasourceServiceContext): QueryPart { + const lcFilters: LilacAlbumCountFilters = { album: { name: this.nowPlaying(ctx).album, artist: { name: this.nowPlaying(ctx).artist }, }, + users: [this.userInput(ctx)], }; - return { query: "albumPlays", variables: { user, lpSettings } }; - } - - artistPlays(ctx: DatasourceServiceContext): QueryPart { - const user: UserInput = { - discordID: ctx.constants.resources!.dbUser.discordID, - }; - const apSettings = { - artist: { name: this.nowPlaying(ctx).artist }, + return { + query: QueryPartName.AlbumCount, + variables: { lcFilters }, + transformer(data: LilacAlbumCountsPage) { + return data.albumCounts[0]; + }, }; - - return { query: "artistPlays", variables: { user, apSettings } }; } albumRating(ctx: DatasourceServiceContext): QueryPart { - const user: UserInput = { - discordID: ctx.constants.resources!.dbUser.discordID, - }; - const lrAlbum = { - name: this.nowPlaying(ctx).album, - artist: { name: this.nowPlaying(ctx).artist }, + const lrFilters: LilacRatingsFilters = { + album: { + name: this.nowPlaying(ctx).album, + artist: { name: this.nowPlaying(ctx).artist }, + }, + user: this.userInput(ctx), }; - return { query: "albumRating", variables: { user, lrAlbum } }; + return { + query: QueryPartName.AlbumRating, + variables: { lrFilters }, + transformer(data: LilacRatingsPage) { + return data.ratings[0]; + }, + }; } globalArtistRank(ctx: DatasourceServiceContext): QueryPart { @@ -254,7 +282,10 @@ export class DatasourceService extends BaseService { const arArtist = { name: this.nowPlaying(ctx).artist }; - return { query: "globalArtistRank", variables: { user, arArtist } }; + return { + query: QueryPartName.GlobalArtistRank, + variables: { user, arArtist }, + }; } serverArtistRank(ctx: DatasourceServiceContext): QueryPart { @@ -264,11 +295,11 @@ export class DatasourceService extends BaseService { const arArtist = { name: this.nowPlaying(ctx).artist }; return { - query: "serverArtistRank", + query: QueryPartName.ServerArtistRank, variables: { user, arArtist, - serverID: ctx.constants.resources!.payload.guild?.id, + guildID: ctx.constants.resources!.payload.guild?.id, }, }; } @@ -284,6 +315,12 @@ export class DatasourceService extends BaseService { ctx.mutable.tagConsolidator.addTags(ctx, tags); } + + private userInput(ctx: DatasourceServiceContext): LilacUserInput { + return { + discordID: ctx.constants.resources!.dbUser.discordID, + }; + } } class GraphQLDatasource { @@ -304,11 +341,21 @@ class GraphQLDatasource { const { query, variables } = buildQuery(this.parts); return async () => ({ - graphQLData: await datasourceService.mirrorballService.query( - ctx, - query, - variables + graphQLData: this.transformResponse( + await datasourceService.lilacAPIService.query(ctx, query, variables) ), }); } + + private transformResponse(response: Record) { + const newResponse = { ...response }; + + for (const part of this.parts) { + if (part.transformer) { + newResponse[part.query] = part.transformer(newResponse[part.query]); + } + } + + return newResponse; + } } diff --git a/src/lib/nowplaying/DependencyMap.ts b/src/lib/nowplaying/DependencyMap.ts index 6d72c452..d8683063 100644 --- a/src/lib/nowplaying/DependencyMap.ts +++ b/src/lib/nowplaying/DependencyMap.ts @@ -8,17 +8,13 @@ import { } from "../../services/LastFM/converters/InfoTypes"; import { CrownDisplay } from "../../services/dbservices/crowns/CrownsService.types"; import { - MirrorballAlbum, - MirrorballArtist, - MirrorballRating, -} from "../../services/mirrorball/MirrorballTypes"; + LilacAlbumCount, + LilacArtistCount, + LilacRating, + LilacWhoKnowsArtistRank, +} from "../../services/lilac/LilacAPIService.types"; import { Combo } from "../calculators/ComboCalculator"; -type ArtistRank = { - rank: number; - listeners: number; -}; - export type DependencyMap = { // Lastfm data artistInfo: ArtistInfo; @@ -32,10 +28,10 @@ export type DependencyMap = { cachedLovedTrack: CachedLovedTrack | undefined; combo: Combo | undefined; - // Mirrorball data - albumPlays: [{ album: MirrorballAlbum; playcount: number }]; - artistPlays: [{ artist: MirrorballArtist; playcount: number }]; - albumRating: { ratings: [MirrorballRating] }; - globalArtistRank: ArtistRank; - serverArtistRank: ArtistRank; + // Lilac data + albumCount: LilacAlbumCount | undefined; + artistCount: LilacArtistCount | undefined; + albumRating: LilacRating | undefined; + globalArtistRank: Pick; + serverArtistRank: Pick; }; diff --git a/src/lib/nowplaying/buildQuery.ts b/src/lib/nowplaying/buildQuery.ts index 05c970bf..9af6b1c7 100644 --- a/src/lib/nowplaying/buildQuery.ts +++ b/src/lib/nowplaying/buildQuery.ts @@ -1,17 +1,20 @@ import { DocumentNode, gql } from "@apollo/client/core"; +import { SimpleMap } from "../../helpers/types"; -const queryParts = [ - "artistPlays", - "albumPlays", - "albumRating", - "globalArtistRank", - "serverArtistRank", -] as const; -export type QueryPartName = (typeof queryParts)[number]; +export enum QueryPartName { + ArtistCount = "artistCount", + AlbumCount = "albumCount", + AlbumRating = "albumRating", + GlobalArtistRank = "globalArtistRank", + ServerArtistRank = "serverArtistRank", +} + +const queryParts = Object.values(QueryPartName); -export interface QueryPart { +export interface QueryPart { query: QueryPartName; - variables: any; + variables: SimpleMap; + transformer?: (data: T) => TransformClass; } export function isQueryPart(value: any): value is QueryPart { @@ -21,44 +24,48 @@ export function isQueryPart(value: any): value is QueryPart { // Some variables must be defined, so these are defaults const startingVariables = { arArtist: {}, + user: {}, }; const nowPlayingQuery = gql` query nowPlayingQuery( - $artistPlays: Boolean! - $albumPlays: Boolean! + $artistCount: Boolean! + $albumCount: Boolean! $albumRating: Boolean! $globalArtistRank: Boolean! $serverArtistRank: Boolean! $user: UserInput! - $apSettings: ArtistPlaysSettings - $lpSettings: AlbumPlaysSettings - $lrAlbum: AlbumInput + $acFilters: ArtistCountsFilters + $lcFilters: AlbumCountsFilters + $lrFilters: RatingsFilters $arArtist: ArtistInput! - $serverID: String + $guildID: String ) { - artistPlays: artistPlays(user: $user, settings: $apSettings) - @include(if: $artistPlays) { - artist { - name + ${QueryPartName.ArtistCount}: artistCounts(filters: $acFilters) @include(if: $artistCount) { + artistCounts { + artist { + name + } + + playcount + firstScrobbled } - playcount } - albumPlays: albumPlays(user: $user, settings: $lpSettings) - @include(if: $albumPlays) { - album { - name - artist { + ${QueryPartName.AlbumCount}: albumCounts(filters: $lcFilters) @include(if: $albumCount) { + albumCounts { + album { name + artist { + name + } } + + playcount } - playcount } - albumRating: ratings( - settings: { user: $user, album: $lrAlbum, pageInput: { limit: 1 } } - ) @include(if: $albumRating) { + ${QueryPartName.AlbumRating}: ratings(filters: $lrFilters) @include(if: $albumRating) { ratings { rating rateYourMusicAlbum { @@ -68,19 +75,19 @@ const nowPlayingQuery = gql` } } - globalArtistRank: artistRank(artist: $arArtist, userInput: $user) + ${QueryPartName.GlobalArtistRank}: whoKnowsArtistRank(artist: $arArtist, user: $user) @include(if: $globalArtistRank) { rank - listeners + totalListeners } - serverArtistRank: artistRank( + ${QueryPartName.ServerArtistRank}: whoKnowsArtistRank( artist: $arArtist - userInput: $user - serverID: $serverID + user: $user + settings: { guildID: $guildID } ) @include(if: $serverArtistRank) { rank - listeners + totalListeners } } `; diff --git a/src/lib/nowplaying/components/AlbumPlaysComponent.ts b/src/lib/nowplaying/components/AlbumPlaysComponent.ts index 34596bf8..febf8289 100644 --- a/src/lib/nowplaying/components/AlbumPlaysComponent.ts +++ b/src/lib/nowplaying/components/AlbumPlaysComponent.ts @@ -1,7 +1,7 @@ import { displayNumber } from "../../ui/displays"; import { BaseNowPlayingComponent } from "../base/BaseNowPlayingComponent"; -const albumPlaysDependencies = ["albumPlays"] as const; +const albumPlaysDependencies = ["albumCount"] as const; export class AlbumPlaysComponent extends BaseNowPlayingComponent< typeof albumPlaysDependencies @@ -11,11 +11,9 @@ export class AlbumPlaysComponent extends BaseNowPlayingComponent< readonly dependencies = albumPlaysDependencies; render() { - const albumPlays = this.values.albumPlays[0]; - return { string: displayNumber( - albumPlays ? albumPlays.playcount : 0, + this.values.albumCount ? this.values.albumCount.playcount : 0, "album scrobble" ), size: 1, diff --git a/src/lib/nowplaying/components/ArtistPlaysComponent.ts b/src/lib/nowplaying/components/ArtistPlaysComponent.ts index 4bb867f1..1a4fe841 100644 --- a/src/lib/nowplaying/components/ArtistPlaysComponent.ts +++ b/src/lib/nowplaying/components/ArtistPlaysComponent.ts @@ -2,7 +2,7 @@ import { displayNumber } from "../../ui/displays"; import { BaseNowPlayingComponent } from "../base/BaseNowPlayingComponent"; import { getArtistPlays } from "../helpers/artist"; -const artistPlaysDependencies = ["artistInfo", "artistPlays"] as const; +const artistPlaysDependencies = ["artistInfo", "artistCount"] as const; export class ArtistPlaysComponent extends BaseNowPlayingComponent< typeof artistPlaysDependencies diff --git a/src/lib/nowplaying/components/GlobalArtistRankComponent.ts b/src/lib/nowplaying/components/GlobalArtistRankComponent.ts index 5cdbf441..2981f2e3 100644 --- a/src/lib/nowplaying/components/GlobalArtistRankComponent.ts +++ b/src/lib/nowplaying/components/GlobalArtistRankComponent.ts @@ -17,7 +17,7 @@ export class GlobalArtistRankComponent extends BaseNowPlayingComponent< if (artistRank && artistRank.rank != -1) { return { string: `Global rank: ${getOrdinal(artistRank.rank)}/${displayNumber( - artistRank.listeners + artistRank.totalListeners )}`, size: 1, }; diff --git a/src/lib/nowplaying/components/RatingComponent.ts b/src/lib/nowplaying/components/RatingComponent.ts index 00189122..6ac2d2ad 100644 --- a/src/lib/nowplaying/components/RatingComponent.ts +++ b/src/lib/nowplaying/components/RatingComponent.ts @@ -11,11 +11,9 @@ export class RatingComponent extends BaseNowPlayingComponent< readonly dependencies = ratingDependencies; render() { - const albumRating = this.values.albumRating.ratings[0]; - - if (albumRating) { + if (this.values.albumRating) { return { - string: displayPlainRating(albumRating.rating), + string: displayPlainRating(this.values.albumRating.rating), size: 1, }; } diff --git a/src/lib/nowplaying/components/ServerRankComponent.ts b/src/lib/nowplaying/components/ServerRankComponent.ts index 78173448..01202ba2 100644 --- a/src/lib/nowplaying/components/ServerRankComponent.ts +++ b/src/lib/nowplaying/components/ServerRankComponent.ts @@ -14,10 +14,12 @@ export class ServerArtistRankComponent extends BaseNowPlayingComponent< render() { const artistRank = this.values.serverArtistRank; + console.log(artistRank); + if (artistRank && artistRank.rank != -1) { return { string: `Server rank: ${getOrdinal(artistRank.rank)}/${displayNumber( - artistRank.listeners + artistRank.totalListeners )}`, size: 1, }; diff --git a/src/lib/nowplaying/compoundComponents/ArtistPlaysAndCrown.ts b/src/lib/nowplaying/compoundComponents/ArtistPlaysAndCrown.ts index 9d4976ad..83482679 100644 --- a/src/lib/nowplaying/compoundComponents/ArtistPlaysAndCrown.ts +++ b/src/lib/nowplaying/compoundComponents/ArtistPlaysAndCrown.ts @@ -2,8 +2,9 @@ import { DiscordService } from "../../../services/Discord/DiscordService"; import { ServiceRegistry } from "../../../services/ServicesRegistry"; import { displayNumber } from "../../ui/displays"; import { BaseCompoundComponent } from "../base/BaseNowPlayingComponent"; +import { getArtistPlays } from "../helpers/artist"; -const dependencies = ["artistInfo", "artistCrown"] as const; +const dependencies = ["artistInfo", "artistCount", "artistCrown"] as const; export class ArtistPlaysAndCrownComponent extends BaseCompoundComponent< typeof dependencies @@ -41,11 +42,10 @@ export class ArtistPlaysAndCrownComponent extends BaseCompoundComponent< let artistPlaysString = ""; let artistExists = false; - if (this.values.artistInfo) { - artistPlaysString = `${displayNumber( - this.values.artistInfo.userPlaycount, - `${this.values.artistInfo.name} scrobble` - )}`; + const { plays, name } = getArtistPlays(this.values); + + if (plays !== undefined && name) { + artistPlaysString = `${displayNumber(plays, `${name} scrobble`)}`; artistExists = true; } else { artistPlaysString = `No data on last.fm for ${this.nowPlaying.artist}`; diff --git a/src/lib/nowplaying/helpers/artist.ts b/src/lib/nowplaying/helpers/artist.ts index f3a46497..fbcbdb87 100644 --- a/src/lib/nowplaying/helpers/artist.ts +++ b/src/lib/nowplaying/helpers/artist.ts @@ -1,4 +1,6 @@ -export function getArtistPlays(values: any): { +import { DependencyMap } from "../DependencyMap"; + +export function getArtistPlays(values: Partial): { plays: number | undefined; name: string | undefined; } { @@ -7,10 +9,10 @@ export function getArtistPlays(values: any): { plays: values.artistInfo.userPlaycount, name: values.artistInfo.name, }; - } else if (values.artistPlays.length) { + } else if (values.artistCount) { return { - plays: values.artistPlays[0].playcount, - name: values.artistPlays[0].artist.name, + plays: values.artistCount.playcount, + name: values.artistCount.artist.name, }; } diff --git a/src/lib/nowplaying/mockDependencies.ts b/src/lib/nowplaying/mockDependencies.ts index 2ff75712..80336fa6 100644 --- a/src/lib/nowplaying/mockDependencies.ts +++ b/src/lib/nowplaying/mockDependencies.ts @@ -160,35 +160,41 @@ export const mockDependencies = ( combo: undefined, // Mirrorball types - albumPlays: [ - { - album: { name: nowPlaying.album, artist: { name: nowPlaying.artist } }, - playcount: 605, - }, - ], - artistPlays: [ - { - artist: { name: nowPlaying.artist }, - playcount: 6360, + albumCount: { + user: {} as any, + album: { + id: 2, + name: nowPlaying.album, + artist: { id: 1, name: nowPlaying.artist, tags: [] }, }, - ], + lastScrobbled: new Date().getTime(), + firstScrobbled: new Date().getTime(), + playcount: 605, + }, + artistCount: { + user: {} as any, + artist: { id: 1, name: nowPlaying.artist, tags: [] }, + playcount: 6360, + lastScrobbled: new Date().getTime(), + firstScrobbled: new Date().getTime(), + }, albumRating: { - ratings: [ - { - rating: 7, - rateYourMusicAlbum: { - rateYourMusicID: "", - title: nowPlaying.album, - artistName: nowPlaying.artist, - releaseYear: 2014, - }, - }, - ], + rating: 7, + rateYourMusicAlbum: { + rateYourMusicID: "12412", + title: nowPlaying.album, + artistName: nowPlaying.artist, + artistNativeName: "", + releaseYear: 2014, + }, }, serverArtistRank: { rank: 4, - listeners: 678, + totalListeners: 678, + }, + globalArtistRank: { + rank: 4, + totalListeners: 678, }, - globalArtistRank: { rank: 4, listeners: 678 }, }; }; diff --git a/src/lib/ui/displays.ts b/src/lib/ui/displays.ts index f3de6d99..1827fe19 100644 --- a/src/lib/ui/displays.ts +++ b/src/lib/ui/displays.ts @@ -138,8 +138,8 @@ export function displayProgressBar( const options = Object.assign( { width: 10, - progressEmoji: "🟩", - remainingEmoji: "⬜", + progressEmoji: Emoji.progress, + remainingEmoji: Emoji.remainingProgress, }, displayOptions ); diff --git a/src/lib/ui/views/EmbedView.ts b/src/lib/ui/views/EmbedView.ts index 4b2e4883..691ea91b 100644 --- a/src/lib/ui/views/EmbedView.ts +++ b/src/lib/ui/views/EmbedView.ts @@ -10,8 +10,8 @@ import { User, } from "discord.js"; import { LineConsolidator } from "../../LineConsolidator"; -import { Image } from "../Image"; import { displayUserTag } from "../displays"; +import { Image } from "../Image"; import { DiscordSendable, View, ViewOptions } from "./View"; export interface Transformable { diff --git a/src/lib/ui/views/ScrollingView.ts b/src/lib/ui/views/ScrollingView.ts index 732f3df4..75175264 100644 --- a/src/lib/ui/views/ScrollingView.ts +++ b/src/lib/ui/views/ScrollingView.ts @@ -8,6 +8,7 @@ import { import { ReactionCollectorFilter } from "../../../helpers/discord"; import { GowonContext } from "../../context/Context"; import { EmojiRaw } from "../../emoji/Emoji"; +import { displayNumber } from "../displays"; import { EmbedView } from "./EmbedView"; import { View } from "./View"; @@ -121,7 +122,7 @@ export class ScrollingView extends View { if (this.options.totalItems >= 0) { if (this.options.totalPages >= 0) footer += " • "; - footer += `${this.options.totalItems} ${ + footer += `${displayNumber(this.options.totalItems)} ${ this.options.totalItems === 1 ? this.options.itemName : this.options.itemNamePlural diff --git a/src/lib/ui/views/SyncingProgressView.ts b/src/lib/ui/views/SyncingProgressView.ts new file mode 100644 index 00000000..67e61a31 --- /dev/null +++ b/src/lib/ui/views/SyncingProgressView.ts @@ -0,0 +1,115 @@ +import { Observable, ObservableSubscription } from "@apollo/client"; +import { Stopwatch } from "../../../helpers"; +import { ServiceRegistry } from "../../../services/ServicesRegistry"; +import { UsersService } from "../../../services/dbservices/UsersService"; +import { + LilacProgressAction, + LilacProgressStage, + SyncProgress, +} from "../../../services/lilac/LilacAPIService.types"; +import { LilacBaseCommand } from "../../Lilac/LilacBaseCommand"; +import { GowonContext } from "../../context/Context"; +import { Emoji } from "../../emoji/Emoji"; +import { displayNumber, displayProgressBar } from "../displays"; +import { SuccessEmbed } from "../embeds/SuccessEmbed"; +import { EmbedView } from "./EmbedView"; +import { UnsendableView } from "./View"; + +export class SyncingProgressView extends UnsendableView { + private stopwatch = new Stopwatch(); + private subscription?: ObservableSubscription; + + private get usersService() { + return ServiceRegistry.get(UsersService); + } + + constructor( + private ctx: GowonContext, + private embed: EmbedView, + private observable: Observable + ) { + super(); + } + + public subscribeToObservable(sendInitialMessage = true): this { + this.stopwatch.start(); + + this.subscription = this.observable.subscribe(async (progress) => { + await this.handleProgress(progress); + }); + + if (sendInitialMessage) { + this.embed + .setDescription( + `Syncing...\n${displayProgressBar(0, 1, { + width: LilacBaseCommand.progressBarWidth, + })}\n*Loading...*` + ) + .editMessage(this.ctx); + } + + return this; + } + + private async handleProgress(progress: SyncProgress) { + if ( + progress.current === progress.total && + progress.stage == LilacProgressStage.Inserting + ) { + await this.handleInsertingComplete(progress); + } else if (this.stopwatch.elapsedInMilliseconds >= 3000) { + await this.handleProgressUpdate(progress); + } + } + + private async handleInsertingComplete(progress: SyncProgress) { + await this.usersService.setIndexed(this.ctx, this.ctx.author.id); + await this.embed + .convert(SuccessEmbed) + .setDescription(this.getInsertingCompleteMessage(progress)) + .editMessage(this.ctx); + + this.subscription?.unsubscribe(); + } + + private async handleProgressUpdate(progress: SyncProgress) { + await this.embed + .setDescription( + `${ + progress.action === LilacProgressAction.Syncing + ? "Syncing" + : "Updating" + }... +${displayProgressBar(progress.current, progress.total, { + width: LilacBaseCommand.progressBarWidth, + progressEmoji: + progress.stage === LilacProgressStage.Fetching + ? Emoji.progress + : Emoji.moreProgress, + remainingEmoji: + progress.stage === LilacProgressStage.Fetching + ? Emoji.remainingProgress + : Emoji.progress, +})} +*${displayNumber(progress.current)}/${displayNumber(progress.total)} ${ + progress.stage === LilacProgressStage.Fetching + ? "scrobbles fetched" + : "counts inserted" + }*` + ) + .setFooter(this.getFooterMessage()) + .editMessage(this.ctx); + + this.stopwatch.zero().start(); + } + + private getInsertingCompleteMessage(progress: SyncProgress): string { + return progress.action === LilacProgressAction.Syncing + ? `Successfully synced your Last.fm data!` + : `Successfully updated your synced data!`; + } + + private getFooterMessage(): string { + return `Syncing means downloading all your Last.fm data. This is required for many commands to function.`; + } +} diff --git a/src/lib/ui/views/View.ts b/src/lib/ui/views/View.ts index 12022225..5ff1029f 100644 --- a/src/lib/ui/views/View.ts +++ b/src/lib/ui/views/View.ts @@ -4,7 +4,10 @@ import { MessageEmbed, MessageOptions, } from "discord.js"; -import { ViewHasNotBeenSentError } from "../../../errors/ui"; +import { + ViewCannotBeSentError, + ViewHasNotBeenSentError, +} from "../../../errors/ui"; import { DiscordService } from "../../../services/Discord/DiscordService"; import { ServiceRegistry } from "../../../services/ServicesRegistry"; import { GowonContext } from "../../context/Context"; @@ -101,3 +104,9 @@ function chainHooks(hook: T | undefined, incoming: T): T { await Promise.resolve(incoming(...args)); }) as any as T; } + +export abstract class UnsendableView extends View { + asDiscordSendable(): DiscordSendable { + throw new ViewCannotBeSentError(); + } +} diff --git a/src/services/Discord/WhoKnowsService.ts b/src/services/Discord/WhoKnowsService.ts index 720eaaf0..e8f19c59 100644 --- a/src/services/Discord/WhoKnowsService.ts +++ b/src/services/Discord/WhoKnowsService.ts @@ -9,10 +9,6 @@ import { LilacUser } from "../lilac/converters/user"; import { LilacPrivacy } from "../lilac/LilacAPIService.types"; import { LilacGuildsService } from "../lilac/LilacGuildsService"; import { PrivateUserDisplay } from "../lilac/LilacUsersService"; -import { - MirrorballPrivacy, - MirrorballUser, -} from "../mirrorball/MirrorballTypes"; import { RedisService, RedisServiceContextOptions, @@ -65,10 +61,10 @@ export class WhoKnowsService extends BaseService { displayUser( ctx: GowonContext, - user: MirrorballUser | LilacUser, + user: LilacUser, options?: Partial ): string { - let nickname = this.nicknameService.cacheGetNickname(ctx, user.discordID); + const nickname = this.nicknameService.cacheGetNickname(ctx, user.discordID); const profileLink = options?.customProfileLink || LastfmLinks.userPage(user.username); @@ -86,17 +82,15 @@ export class WhoKnowsService extends BaseService { } switch (user.privacy) { - case MirrorballPrivacy.Discord: case LilacPrivacy.Discord: return ( this.nicknameService.cacheGetUsername(ctx, user.discordID) || UnknownUserDisplay ); - case MirrorballPrivacy.FMUsername: case LilacPrivacy.FMUsername: return Emoji.lastfm + " " + displayLink(user.username, profileLink); - case MirrorballPrivacy.Both: + case LilacPrivacy.Both: return displayLink( this.nicknameService.cacheGetUsername(ctx, user.discordID) || @@ -104,8 +98,6 @@ export class WhoKnowsService extends BaseService { profileLink ); - case MirrorballPrivacy.Private: - case MirrorballPrivacy.Unset: case LilacPrivacy.Private: case LilacPrivacy.Unset: return PrivateUserDisplay; diff --git a/src/services/ServicesRegistry.ts b/src/services/ServicesRegistry.ts index 9c32897e..7cd990bc 100644 --- a/src/services/ServicesRegistry.ts +++ b/src/services/ServicesRegistry.ts @@ -41,10 +41,13 @@ import { FishyService } from "./fishy/FishyService"; import { FishyProgressionService } from "./fishy/quests/FishyProgressionService"; import { IntervaledJobsService } from "./intervaledJobs/IntervaledJobsService"; import { LilacAPIService } from "./lilac/LilacAPIService"; +import { LilacAlbumsService } from "./lilac/LilacAlbumsService"; import { LilacArtistsService } from "./lilac/LilacArtistsService"; import { LilacGuildsService } from "./lilac/LilacGuildsService"; import { LilacLibraryService } from "./lilac/LilacLibraryService"; +import { LilacRatingsService } from "./lilac/LilacRatingsService"; import { LilacTagsService } from "./lilac/LilacTagsService"; +import { LilacTracksService } from "./lilac/LilacTracksService"; import { LilacUsersService } from "./lilac/LilacUsersService"; import { LilacWhoKnowsService } from "./lilac/LilacWhoKnowsService"; import { MirrorballService } from "./mirrorball/MirrorballService"; @@ -84,9 +87,12 @@ const services: Service[] = [ // Lilac services LilacAPIService, LilacArtistsService, + LilacAlbumsService, LilacGuildsService, LilacLibraryService, + LilacRatingsService, LilacTagsService, + LilacTracksService, LilacUsersService, LilacWhoKnowsService, diff --git a/src/services/arguments/mentions/MentionsService.ts b/src/services/arguments/mentions/MentionsService.ts index 04cffa41..920c9763 100644 --- a/src/services/arguments/mentions/MentionsService.ts +++ b/src/services/arguments/mentions/MentionsService.ts @@ -1,5 +1,6 @@ import { User as DiscordUser } from "discord.js"; import { + CommandRequiresBackerError, MentionedSignInRequiredError, MentionedUserNotAuthenticatedError, MentionedUserNotIndexedError, @@ -133,10 +134,13 @@ export class MentionsService extends BaseService { ): Promise { if (options.usernameRequired) this.ensureUsername(ctx, mentionsBuilder); if (options.senderRequired) this.ensureSender(ctx, mentionsBuilder); - if (options.indexedRequired) await this.ensureIndexed(ctx, mentionsBuilder); + if (options.syncedRequired) await this.ensureIndexed(ctx, mentionsBuilder); if (options.lfmAuthentificationRequired) { this.ensureUserAuthenticated(ctx, requestables, mentionsBuilder); } + if (options.backerRequired) { + this.ensurePremium(ctx, mentionsBuilder); + } } private async maybeHandleReply( @@ -276,7 +280,9 @@ export class MentionsService extends BaseService { const discordID = mentionsBuilder.getDiscordID("mentioned"); if (discordID) { - userPromises.push(this.lilacUsersService.fetch(ctx, { discordID })); + userPromises.push( + this.lilacUsersService.fetch(ctx, { discordID: discordID }) + ); } const users = await Promise.all(userPromises); @@ -345,4 +351,10 @@ export class MentionsService extends BaseService { } } } + + private ensurePremium(ctx: GowonContext, mentionsBuilder: MentionsBuilder) { + if (mentionsBuilder.getDBUser()?.hasPremium === false) { + throw new CommandRequiresBackerError(ctx.command.prefix); + } + } } diff --git a/src/services/arguments/mentions/MentionsService.types.ts b/src/services/arguments/mentions/MentionsService.types.ts index 01d198f6..0842e671 100644 --- a/src/services/arguments/mentions/MentionsService.types.ts +++ b/src/services/arguments/mentions/MentionsService.types.ts @@ -21,11 +21,13 @@ export interface GetMentionsOptions { /** If set to true, throws an error if no username is provided */ usernameRequired: boolean; /** If set to true, throws an error if the returned user isn't indexed */ - indexedRequired?: boolean; + syncedRequired?: boolean; /** If set to true, throws an error if the user is not authenticated with Last.fm */ lfmAuthentificationRequired?: boolean; /** If set to true, throws an error if a database user is not found */ dbUserRequired?: boolean; + /** If set to true, throws an error if the user does not have premium */ + backerRequired?: boolean; /** @deprecated Use `dbUserRequired` instead */ reverseLookup: { diff --git a/src/services/dbservices/UsersService.ts b/src/services/dbservices/UsersService.ts index 6c4f5e87..7fccd7f8 100644 --- a/src/services/dbservices/UsersService.ts +++ b/src/services/dbservices/UsersService.ts @@ -16,12 +16,17 @@ import { Requestable } from "../LastFM/LastFMAPIService"; import { LastFMSession } from "../LastFM/converters/Misc"; import { ServiceRegistry } from "../ServicesRegistry"; import { buildRequestable } from "../arguments/mentions/MentionsBuilder"; +import { LilacUsersService } from "../lilac/LilacUsersService"; export class UsersService extends BaseService { get analyticsCollector() { return ServiceRegistry.get(AnalyticsCollector); } + get lilacUsersService() { + return ServiceRegistry.get(LilacUsersService); + } + async getUsername(ctx: GowonContext, discordID: string): Promise { this.log(ctx, `fetching username with discordID ${discordID}`); @@ -191,7 +196,7 @@ export class UsersService extends BaseService { await user.save(); } - async setPatron(ctx: GowonContext, discordID: string, value: boolean) { + async setAsBacker(ctx: GowonContext, discordID: string, value: boolean) { this.log(ctx, `Setting user with id ${discordID} as a patron`); const user = await this.getUser(ctx, discordID); @@ -199,6 +204,12 @@ export class UsersService extends BaseService { user.isPatron = value; await user.save(); + + await this.lilacUsersService.modify( + ctx, + { discordID: user.discordID }, + { hasPremium: value } + ); } async setRoles( diff --git a/src/services/lilac/LilacAPIService.ts b/src/services/lilac/LilacAPIService.ts index cfb87cdb..d00ad678 100644 --- a/src/services/lilac/LilacAPIService.ts +++ b/src/services/lilac/LilacAPIService.ts @@ -6,7 +6,7 @@ import { lilacClient } from "../../lib/Lilac/client"; import { BaseService } from "../BaseService"; export class LilacAPIService extends BaseService { - protected async query( + public async query( ctx: GowonContext, query: DocumentNode, variables?: V, diff --git a/src/services/lilac/LilacAPIService.types.ts b/src/services/lilac/LilacAPIService.types.ts index 28fe98e6..e9c99b9c 100644 --- a/src/services/lilac/LilacAPIService.types.ts +++ b/src/services/lilac/LilacAPIService.types.ts @@ -1,11 +1,19 @@ // Types -import { ArtistInput } from "../mirrorball/MirrorballTypes"; import { LilacUser } from "./converters/user"; export type LilacDate = number; -export type LilacProgressAction = "indexing" | "updating"; +export enum LilacProgressAction { + Syncing = "sync", + Updating = "update", +} + +export enum LilacProgressStage { + Fetching = "fetching", + Inserting = "inserting", + Terminated = "terminated", +} export enum LilacPrivacy { Private = "PRIVATE", @@ -28,6 +36,7 @@ export interface LilacUserModifications { username?: string; privacy?: LilacPrivacy; lastFMSession?: string; + hasPremium?: boolean; } export interface LilacArtistInput { @@ -55,6 +64,13 @@ export interface LilacWhoKnowsInput { userIDs?: string[]; } +export interface LilacWhoFirstInput { + guildID?: string; + limit?: number; + userIDs?: string[]; + reverse?: boolean; +} + export interface LilacPaginationInput { page: number; perPage: number; @@ -83,6 +99,11 @@ export interface LilacArtistCountFilters { tags?: LilacTagInput[]; } +export interface LilacAlbumFilters { + album?: LilacAlbumInput; + pagination?: LilacPaginationInput; +} + export interface LilacAlbumCountFilters { album?: LilacAlbumInput; users?: LilacUserInput[]; @@ -97,18 +118,26 @@ export interface LilacTrackCountFilters { export interface LilacTagsFilters { inputs?: LilacTagInput[]; - artists?: ArtistInput[]; + artists?: LilacArtistInput[]; pagination?: LilacPaginationInput; fetchTagsForMissing?: boolean; } +export interface LilacRatingsFilters { + album?: LilacAlbumInput; + user?: LilacUserInput; + rating?: number; + pagination?: LilacPaginationInput; +} + // Responses -export interface IndexingProgress< +export interface SyncProgress< Action extends LilacProgressAction = LilacProgressAction > { action: Action; - page: number; - totalPages: number; + stage: LilacProgressStage; + current: number; + total: number; } export interface LilacPagination { @@ -121,10 +150,10 @@ export interface LilacPagination { export interface RawLilacUser { id: number; username: string; - discordId: string; + discordID: string; privacy: LilacPrivacy; - lastIndexed?: LilacDate; - isIndexing?: boolean; + lastSynced?: LilacDate; + isSyncing?: boolean; } export interface LilacWhoKnowsArtistRank { @@ -157,6 +186,14 @@ export interface LilacWhoKnowsTrackRank { below: LilacAmbiguousTrackCount; } +export interface LIlacWhoFirstArtistRank { + artist: LilacArtist; + firstScrobbled: LilacDate; + lastScrobbled: LilacDate; + rank: number; + totalListeners: number; +} + // Objects export interface LilacArtist { id: number; @@ -168,7 +205,7 @@ export interface LilacArtist { export interface LilacAlbum { id: number; name: string; - artist: LilacAlbum; + artist: LilacArtist; } export interface LilacTrack { @@ -202,16 +239,26 @@ export interface LilacWhoKnowsRow { playcount: number; } +export interface LilacWhoFirstRow { + user: LilacUser; + firstScrobbled: LilacDate; + lastScrobbled: LilacDate; +} + export interface LilacArtistCount { user: LilacUser; playcount: number; artist: LilacArtist; + firstScrobbled: LilacDate; + lastScrobbled: LilacDate; } export interface LilacAlbumCount { user: LilacUser; playcount: number; album: LilacAlbum; + firstScrobbled: LilacDate; + lastScrobbled: LilacDate; } export interface LilacTrackCount { @@ -223,9 +270,31 @@ export interface LilacTrackCount { export interface LilacAmbiguousTrackCount { user: LilacUser; playcount: number; - album: LilacAlbum; + track: LilacAmbiguousTrack; + firstScrobbled: LilacDate; + lastScrobbled: LilacDate; +} + +export interface LilacRating { + rating: number; + rateYourMusicAlbum: LilacRateYourMusicAlbum; } +export interface LilacRateYourMusicAlbum { + rateYourMusicID: string; + title: string; + artistName: string; + artistNativeName: string; + releaseYear: number; +} + +export interface LilacRateYourMusicArtist { + artistName: string; + artistNativeName: string; +} + +// Pages + export interface LilacScrobblesPage { pagination: LilacPagination; scrobbles: LilacScrobble[]; @@ -241,6 +310,11 @@ export interface LilacArtistCountsPage { artistCounts: LilacArtistCount[]; } +export interface LilacAlbumsPage { + pagination: LilacPagination; + albums: LilacAlbum[]; +} + export interface LilacAlbumCountsPage { pagination: LilacPagination; albumCounts: LilacAlbumCount[]; @@ -251,7 +325,17 @@ export interface LilacTrackCountsPage { trackCounts: LilacTrackCount[]; } +export interface LilacAmbiguousTrackCountsPage { + pagination: LilacPagination; + trackCounts: LilacAmbiguousTrackCount[]; +} + export interface LilacTagsPage { pagination: LilacPagination; tags: LilacTag[]; } + +export interface LilacRatingsPage { + pagination: LilacPagination; + ratings: LilacRating[]; +} diff --git a/src/services/lilac/LilacAlbumsService.ts b/src/services/lilac/LilacAlbumsService.ts new file mode 100644 index 00000000..d9d84049 --- /dev/null +++ b/src/services/lilac/LilacAlbumsService.ts @@ -0,0 +1,66 @@ +import { gql } from "apollo-server-express"; +import { GowonContext } from "../../lib/context/Context"; +import { LilacAPIService } from "./LilacAPIService"; +import { LilacAlbumFilters, LilacAlbumsPage } from "./LilacAPIService.types"; + +type SimpleLilacAlbum = { + artist: string; + name: string; +}; + +export class LilacAlbumsService extends LilacAPIService { + async list( + ctx: GowonContext, + filters: LilacAlbumFilters + ): Promise { + const query = gql` + query list($filters: AlbumsFilters!) { + albums(filters: $filters) { + albums { + id + name + + artist { + name + } + } + + pagination { + totalItems + currentPage + totalPages + perPage + } + } + } + `; + + const response = await this.query< + { albums: LilacAlbumsPage }, + { filters: LilacAlbumFilters } + >(ctx, query, { filters }, false); + + return response.albums; + } + + async correctAlbumName( + ctx: GowonContext, + album: SimpleLilacAlbum + ): Promise { + this.log(ctx, `Correcting album name for ${album.artist} - ${album.name}`); + + const albumInput = { + name: album.name, + artist: { name: album.artist }, + }; + + const response = await this.list(ctx, { album: albumInput }); + + return response.albums.length > 0 + ? { + artist: response.albums[0].artist.name, + name: response.albums[0].name, + } + : album; + } +} diff --git a/src/services/lilac/LilacArtistsService.ts b/src/services/lilac/LilacArtistsService.ts index 781bccde..68932a02 100644 --- a/src/services/lilac/LilacArtistsService.ts +++ b/src/services/lilac/LilacArtistsService.ts @@ -3,6 +3,7 @@ import { GowonContext } from "../../lib/context/Context"; import { displayNumber } from "../../lib/ui/displays"; import { LilacAPIService } from "./LilacAPIService"; import { + LilacArtistCount, LilacArtistCountFilters, LilacArtistCountsPage, LilacArtistFilters, @@ -97,6 +98,8 @@ export class LilacArtistsService extends LilacAPIService { name } playcount + lastScrobbled + firstScrobbled } pagination { @@ -117,6 +120,19 @@ export class LilacArtistsService extends LilacAPIService { return response.artistCounts; } + public async getCount( + ctx: GowonContext, + discordID: string, + artist: string + ): Promise { + const response = await this.listCounts(ctx, { + users: [{ discordID }], + artists: [{ name: artist }], + }); + + return response.artistCounts[0]; + } + async getTagsForArtistsMap( ctx: GowonContext, artists: string[], diff --git a/src/services/lilac/LilacLibraryService.ts b/src/services/lilac/LilacLibraryService.ts index 5e7ba8b2..dc0ab2be 100644 --- a/src/services/lilac/LilacLibraryService.ts +++ b/src/services/lilac/LilacLibraryService.ts @@ -2,14 +2,13 @@ import { gql } from "@apollo/client"; import { GowonContext } from "../../lib/context/Context"; import { LilacAPIService } from "./LilacAPIService"; import { + LilacAlbumCount, LilacAlbumCountFilters, LilacAlbumCountsPage, LilacArtistFilters, LilacArtistsPage, LilacScrobbleFilters, LilacScrobblesPage, - LilacTrackCountFilters, - LilacTrackCountsPage, } from "./LilacAPIService.types"; export class LilacLibraryService extends LilacAPIService { @@ -90,40 +89,6 @@ export class LilacLibraryService extends LilacAPIService { return response; } - async trackCounts( - ctx: GowonContext, - filters: LilacTrackCountFilters - ): Promise { - const query = gql` - query trackCounts($filters: TrackCountsFilters!) { - trackCounts(filters: $filters) { - trackCounts { - playcount - - track { - name - - artist { - name - } - - album { - name - } - } - } - } - } - `; - - const response = await this.query< - { trackCounts: LilacTrackCountsPage }, - { filters: LilacTrackCountFilters } - >(ctx, query, { filters }, false); - - return response.trackCounts; - } - public async getScrobbleCount( ctx: GowonContext, discordID: string @@ -148,4 +113,45 @@ export class LilacLibraryService extends LilacAPIService { return response?.scrobbles?.pagination?.totalItems; } + + public async getAlbumCount( + ctx: GowonContext, + discordID: string, + artist: string, + album: string + ): Promise { + const query = gql` + query albumCount($filters: AlbumCountsFilters!) { + albumCounts(filters: $filters) { + albumCounts { + playcount + lastScrobbled + firstScrobbled + + album { + name + + artist { + name + } + } + } + } + } + `; + + const response = await this.query< + { + albumCounts: { albumCounts: LilacAlbumCount[] }; + }, + { filters: LilacAlbumCountFilters } + >(ctx, query, { + filters: { + users: [{ discordID }], + album: { name: album, artist: { name: artist } }, + }, + }); + + return response.albumCounts.albumCounts[0]; + } } diff --git a/src/services/lilac/LilacRatingsService.ts b/src/services/lilac/LilacRatingsService.ts new file mode 100644 index 00000000..7ed42162 --- /dev/null +++ b/src/services/lilac/LilacRatingsService.ts @@ -0,0 +1,117 @@ +import { gql } from "apollo-server-express"; +import { GowonContext } from "../../lib/context/Context"; +import { LilacAPIService } from "./LilacAPIService"; +import { + LilacRateYourMusicArtist, + LilacRatingsFilters, + LilacRatingsPage, + LilacUserInput, +} from "./LilacAPIService.types"; + +export class LilacRatingsService extends LilacAPIService { + async ratings( + ctx: GowonContext, + filters: LilacRatingsFilters + ): Promise { + const query = gql` + query ratings($filters: RatingsFilters!) { + ratings(filters: $filters) { + ratings { + rateYourMusicAlbum { + title + artistName + artistNativeName + releaseYear + } + rating + } + + pagination { + totalItems + totalPages + currentPage + perPage + } + } + } + `; + + const response = await this.query< + { ratings: LilacRatingsPage }, + { filters: LilacRatingsFilters } + >(ctx, query, { filters }, false); + + return response.ratings; + } + + async getArtist( + ctx: GowonContext, + keywords: string + ): Promise { + const query = gql` + query rateYourMusicArtist($keywords: String!) { + rateYourMusicArtist(keywords: $keywords) { + artistName + artistNativeName + } + } + `; + + const response = await this.query< + { rateYourMusicArtist: LilacRateYourMusicArtist }, + { keywords: string } + >(ctx, query, { keywords }); + + return response.rateYourMusicArtist; + } + + async ratingsTaste( + ctx: GowonContext, + variables: { sender: LilacUserInput; mentioned: LilacUserInput } + ): Promise<{ + sender: LilacRatingsPage; + mentioned: LilacRatingsPage; + }> { + const query = gql` + query stats($sender: UserInput, $mentioned: UserInput) { + sender: ratings(filters: { user: $sender }) { + ratings { + rating + rateYourMusicAlbum { + rateYourMusicID + title + artistName + } + } + pagination { + totalItems + } + } + + mentioned: ratings(filters: { user: $mentioned }) { + ratings { + rating + rateYourMusicAlbum { + rateYourMusicID + title + artistName + } + } + pagination { + totalItems + } + } + } + `; + + const response = await this.query< + { + sender: LilacRatingsPage; + mentioned: LilacRatingsPage; + }, + { sender: LilacUserInput; mentioned: LilacUserInput } + >(ctx, query, variables); + + return response; + } +} diff --git a/src/services/lilac/LilacTagsService.ts b/src/services/lilac/LilacTagsService.ts index 698505fe..50ad9f49 100644 --- a/src/services/lilac/LilacTagsService.ts +++ b/src/services/lilac/LilacTagsService.ts @@ -2,9 +2,9 @@ import { gql } from "@apollo/client"; import { GowonContext } from "../../lib/context/Context"; import { displayNumber } from "../../lib/ui/displays"; import { RawArtistInfo, RawTagTopArtists } from "../LastFM/LastFMService.types"; -import { ArtistInput } from "../mirrorball/MirrorballTypes"; import { LilacAPIService } from "./LilacAPIService"; import { + LilacArtistInput, LilacTag, LilacTagsFilters, LilacTagsPage, @@ -83,7 +83,7 @@ export class LilacTagsService extends LilacAPIService { async getTagsForArtists( ctx: GowonContext, - artists: ArtistInput[] + artists: LilacArtistInput[] ): Promise { this.log( ctx, diff --git a/src/services/lilac/LilacTracksService.ts b/src/services/lilac/LilacTracksService.ts new file mode 100644 index 00000000..63aca35a --- /dev/null +++ b/src/services/lilac/LilacTracksService.ts @@ -0,0 +1,91 @@ +import { gql } from "apollo-server-express"; +import { GowonContext } from "../../lib/context/Context"; +import { LilacAPIService } from "./LilacAPIService"; +import { + LilacAmbiguousTrackCount, + LilacAmbiguousTrackCountsPage, + LilacTrackCountFilters, + LilacTrackCountsPage, +} from "./LilacAPIService.types"; + +export class LilacTracksService extends LilacAPIService { + public async listCounts( + ctx: GowonContext, + filters: LilacTrackCountFilters + ): Promise { + const query = gql` + query trackCounts($filters: TrackCountsFilters!) { + trackCounts(filters: $filters) { + trackCounts { + playcount + + track { + name + + artist { + name + } + + album { + name + } + } + } + } + } + `; + + const response = await this.query< + { trackCounts: LilacTrackCountsPage }, + { filters: LilacTrackCountFilters } + >(ctx, query, { filters }, false); + + return response.trackCounts; + } + + public async listAmbiguousCounts( + ctx: GowonContext, + filters: LilacTrackCountFilters + ): Promise { + const query = gql` + query ambiguousTrackCounts($filters: TrackCountsFilters!) { + ambiguousTrackCounts(filters: $filters) { + trackCounts { + playcount + firstScrobbled + lastScrobbled + + track { + name + + artist { + name + } + } + } + } + } + `; + + const response = await this.query< + { ambiguousTrackCounts: LilacAmbiguousTrackCountsPage }, + { filters: LilacTrackCountFilters } + >(ctx, query, { filters }, false); + + return response.ambiguousTrackCounts; + } + + public async getAmbiguousCount( + ctx: GowonContext, + discordID: string, + artist: string, + track: string + ): Promise { + const response = await this.listAmbiguousCounts(ctx, { + track: { name: track, artist: { name: artist } }, + users: [{ discordID }], + }); + + return response.trackCounts[0]; + } +} diff --git a/src/services/lilac/LilacUsersService.ts b/src/services/lilac/LilacUsersService.ts index 79ec892a..533c2dc9 100644 --- a/src/services/lilac/LilacUsersService.ts +++ b/src/services/lilac/LilacUsersService.ts @@ -4,24 +4,31 @@ import { LilacUser } from "./converters/user"; import { userToUserInput } from "./helpers"; import { LilacAPIService } from "./LilacAPIService"; import { - IndexingProgress, LilacUserInput, LilacUserModifications, RawLilacUser, + SyncProgress, } from "./LilacAPIService.types"; export const PrivateUserDisplay = "Private user"; export class LilacUsersService extends LilacAPIService { - public async index(ctx: GowonContext, user: LilacUserInput): Promise { - await this.mutate<{ index: any }, { user: LilacUserInput }>( + public async sync( + ctx: GowonContext, + user: LilacUserInput, + forceRestart = false + ): Promise { + await this.mutate< + { index: any }, + { user: LilacUserInput; forceRestart: boolean } + >( ctx, gql` - mutation index($user: UserInput!) { - index(user: $user) + mutation sync($user: UserInput!, $forceRestart: Boolean!) { + sync(user: $user, forceRestart: $forceRestart) } `, - { user: userToUserInput(user) } + { user: userToUserInput(user), forceRestart } ); } @@ -37,26 +44,26 @@ export class LilacUsersService extends LilacAPIService { ); } - public indexingProgress( + public syncProgress( ctx: GowonContext, user: LilacUserInput - ): Observable { + ): Observable { const subscription = gql` - subscription index($user: UserInput!) { - index(user: $user) { + subscription sync($user: UserInput!) { + sync(user: $user) { action - page - totalPages + stage + current + total } } `; - return this.subscribe< - { index: IndexingProgress }, - { user: LilacUserInput } - >(ctx, subscription, { user: userToUserInput(user) }).map( - (data) => data.index - ); + return this.subscribe<{ sync: SyncProgress }, { user: LilacUserInput }>( + ctx, + subscription, + { user: userToUserInput(user) } + ).map((data) => data.sync); } public async fetchAll( @@ -75,7 +82,7 @@ export class LilacUsersService extends LilacAPIService { discordID username privacy - lastIndexed + lastSynced } } `, @@ -105,7 +112,7 @@ export class LilacUsersService extends LilacAPIService { gql` query fetchUsers($filters: UserInput) { users(filters: $filters) { - isIndexing + isSyncing } } `, @@ -113,7 +120,7 @@ export class LilacUsersService extends LilacAPIService { false ); - return users.users[0]?.isIndexing ?? false; + return users.users[0]?.isSyncing ?? false; } public async modify( diff --git a/src/services/lilac/LilacWhoKnowsService.ts b/src/services/lilac/LilacWhoKnowsService.ts index b0aa252b..0b725746 100644 --- a/src/services/lilac/LilacWhoKnowsService.ts +++ b/src/services/lilac/LilacWhoKnowsService.ts @@ -2,6 +2,7 @@ import { gql } from "@apollo/client"; import { GowonContext } from "../../lib/context/Context"; import { LilacAPIService } from "./LilacAPIService"; import { + LIlacWhoFirstArtistRank, LilacAlbum, LilacAlbumInput, LilacAmbiguousTrack, @@ -9,6 +10,8 @@ import { LilacArtistInput, LilacTrackInput, LilacUserInput, + LilacWhoFirstInput, + LilacWhoFirstRow, LilacWhoKnowsAlbumRank, LilacWhoKnowsArtistRank, LilacWhoKnowsInput, @@ -53,7 +56,6 @@ export class LilacWhoKnowsService extends LilacAPIService { username discordID privacy - lastIndexed } } } @@ -72,8 +74,9 @@ export class LilacWhoKnowsService extends LilacAPIService { { artist: { name: artist }, settings: { guildID, limit }, - user: { discordID }, - } + user: { discordID: discordID }, + }, + false ); } @@ -117,7 +120,6 @@ export class LilacWhoKnowsService extends LilacAPIService { username discordID privacy - lastIndexed } } } @@ -132,7 +134,7 @@ export class LilacWhoKnowsService extends LilacAPIService { { album: { name: album, artist: { name: artist } }, settings: { guildID, limit }, - user: { discordID }, + user: { discordID: discordID }, } ); } @@ -177,7 +179,6 @@ export class LilacWhoKnowsService extends LilacAPIService { username discordID privacy - lastIndexed } } } @@ -192,7 +193,69 @@ export class LilacWhoKnowsService extends LilacAPIService { { track: { name: track, artist: { name: artist } }, settings: { guildID, limit }, - user: { discordID }, + user: { discordID: discordID }, + } + ); + } + + public async whoFirstArtist( + ctx: GowonContext, + artist: string, + discordID: string, + guildID?: string, + limit = 15, + reverse = false + ) { + return await this.query< + { + whoFirstArtist: { artist: LilacArtist; rows: LilacWhoFirstRow[] }; + whoFirstArtistRank: LIlacWhoFirstArtistRank; + }, + { + artist: LilacArtistInput; + settings: LilacWhoFirstInput; + user: LilacUserInput; + } + >( + ctx, + gql` + query whoFirstArtist( + $artist: ArtistInput! + $settings: WhoFirstInput! + $user: UserInput! + ) { + whoFirstArtist(artist: $artist, settings: $settings) { + artist { + name + } + + rows { + firstScrobbled + lastScrobbled + user { + username + discordID + privacy + } + } + } + + whoFirstArtistRank( + artist: $artist + settings: $settings + user: $user + ) { + rank + totalListeners + firstScrobbled + lastScrobbled + } + } + `, + { + artist: { name: artist }, + settings: { guildID, limit, reverse }, + user: { discordID: discordID }, } ); } diff --git a/src/services/lilac/converters/user.ts b/src/services/lilac/converters/user.ts index 2a07ff4e..be3b8afe 100644 --- a/src/services/lilac/converters/user.ts +++ b/src/services/lilac/converters/user.ts @@ -6,17 +6,17 @@ export class LilacUser extends BaseLilacConverter { username: string; discordID: string; privacy: LilacPrivacy; - lastUpdated?: Date; - isBeingindexed?: boolean; + lastSynced?: Date; + isSyncing?: boolean; constructor(raw: RawLilacUser) { super(); this.id = raw.id; this.username = raw.username; - this.discordID = raw.discordId; + this.discordID = raw.discordID; this.privacy = raw.privacy; - this.lastUpdated = this.convertDate(raw.lastIndexed); - this.isBeingindexed = raw.isIndexing; + this.lastSynced = this.convertDate(raw.lastSynced); + this.isSyncing = raw.isSyncing; } } diff --git a/src/services/mirrorball/MirrorballService.ts b/src/services/mirrorball/MirrorballService.ts index c95fc425..432f95ee 100644 --- a/src/services/mirrorball/MirrorballService.ts +++ b/src/services/mirrorball/MirrorballService.ts @@ -1,11 +1,11 @@ import { gql } from "@apollo/client/core"; import { DocumentNode } from "graphql"; +import config from "../../../config.json"; +import { MirrorballError } from "../../errors/errors"; import { SimpleMap } from "../../helpers/types"; +import { GowonContext } from "../../lib/context/Context"; import { mirrorballClient } from "../../lib/indexing/client"; import { BaseService } from "../BaseService"; -import config from "../../../config.json"; -import { GowonContext } from "../../lib/context/Context"; -import { MirrorballError } from "../../errors/errors"; export class MirrorballService extends BaseService { private async makeRequest( diff --git a/src/setup.ts b/src/setup.ts index ce9f1dda..96a6ad19 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -8,6 +8,7 @@ import { DB } from "./database/DB"; import { Stopwatch } from "./helpers"; import { uppercaseFirstLetter } from "./helpers/string"; import { GowonClient } from "./lib/GowonClient"; +import { lilacClient } from "./lib/Lilac/client"; import { CommandHandler } from "./lib/command/CommandHandler"; import { CommandRegistry, @@ -73,6 +74,7 @@ export async function setup(ctx: GowonContext) { connectToDB(), connectToRedis(), connectToMirrorball(), + connectToLilac(), intializeAPI(), initializeCommandRegistry(), ]); @@ -126,6 +128,18 @@ function connectToMirrorball() { }, "Connected to Mirrorball"); } +function connectToLilac() { + return logStartup(async () => { + await lilacClient.query({ + query: gql` + query { + ping + } + `, + }); + }, "Connected to Lilac"); +} + function initializeCommandRegistry() { return logStartup( async () => CommandRegistry.getInstance().init(await generateCommands()),