Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[PAY-2533, PAY-2534, PAY-2545, PAY-2547, PAY-2532] Index album purchases #7735

Merged
merged 16 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 92 additions & 40 deletions packages/commands/src/create-playlist.mjs
Original file line number Diff line number Diff line change
@@ -1,49 +1,101 @@
import { randomBytes, randomInt } from "crypto";
import chalk from "chalk";
import { program } from "commander";
import { randomBytes, randomInt } from 'crypto'
import chalk from 'chalk'
import { program } from 'commander'

import { initializeAudiusLibs } from "./utils.mjs";
import { initializeAudiusLibs } from './utils.mjs'

program.command("create-playlist")
.description("Create playlist")
.argument("<trackIds...>", "Tracks to include in playlist")
.option("-n, --name <name>", "Name of playlist (chosen randomly if not specified)")
.option("-a, --album", "Make playlist an album", false)
.option("-d, --description <description>", "Description of playlist (chosen randomly if not specified)")
.option("-p, --private", "Make playlist private", false)
.option("-f, --from <from>", "The account to create playlist from")
.action(async (trackIds, { name, album, description, private: isPrivate, from }) => {
const audiusLibs = await initializeAudiusLibs(from);
const rand = randomBytes(2).toString("hex").padStart(4, "0").toUpperCase();
program
.command('create-playlist')
.description('Create playlist')
.argument('<trackIds...>', 'Tracks to include in playlist')
.option(
'-n, --name <name>',
'Name of playlist (chosen randomly if not specified)'
)
.option('-a, --album', 'Make playlist an album', false)
.option(
'-d, --description <description>',
'Description of playlist (chosen randomly if not specified)'
)
.option('-p, --private', 'Make playlist private', false)
.option(
'-u, --price <price>',
'The price for the album. Cannot be used without --album option'
)
.option('-f, --from <from>', 'The account to create playlist from')
.action(
async (
trackIds,
{ name, album, description, price, private: isPrivate, from }
) => {
const audiusLibs = await initializeAudiusLibs(from)
const rand = randomBytes(2).toString('hex').padStart(4, '0').toUpperCase()

try {
const playlistId = randomInt(400_001, 40_000_000);
const playlistName = name || `playlist ${rand}`
const metadata = {
playlist_id: playlistId,
playlist_name: playlistName,
description: description || `playlist generated by audius-cmd ${rand}`,
is_album: album,
is_private: isPrivate,
playlist_contents: {
track_ids: trackIds.map(trackId => ({
track: Number(trackId),
metadata_time: Date.now() / 1000,
}))
try {
const playlistId = randomInt(400_001, 40_000_000)
const playlistName = name || `playlist ${rand}`
const metadata = {
playlist_id: playlistId,
playlist_name: playlistName,
description:
description || `playlist generated by audius-cmd ${rand}`,
is_album: album,
is_private: isPrivate,
playlist_contents: {
track_ids: trackIds.map((trackId) => ({
track: Number(trackId),
metadata_time: Date.now() / 1000
}))
}
}
console.log(chalk.yellow('Playlist Metadata: '), metadata)
if (price) {
if (!album) {
program.error(chalk.red('Price can only be set for albums'))
}
metadata.is_stream_gated = true
metadata.stream_conditions = await getStreamConditions({
price,
audiusLibs
})
console.log(
chalk.yellow('Stream Conditions: '),
metadata.stream_conditions
)
}
const response = await audiusLibs.EntityManager.createPlaylist(metadata)

if (response.error) {
program.error(chalk.red(response.error))
}
}
const response = await audiusLibs.EntityManager.createPlaylist(metadata)

if (response.error) {
program.error(chalk.red(response.error));
console.log(chalk.green('Successfully created playlist'))
console.log(chalk.yellow('Playlist Name: '), playlistName)
console.log(chalk.yellow('Playlist ID: '), playlistId)
} catch (err) {
program.error(err.message)
}

console.log(chalk.green("Successfully created playlist"));
console.log(chalk.yellow("Playlist Name: "), playlistName)
console.log(chalk.yellow("Playlist ID: "), playlistId)
} catch (err) {
program.error(err.message);
process.exit(0)
}
)

process.exit(0);
});
const getStreamConditions = async ({ price: priceString, audiusLibs }) => {
if (priceString) {
const price = Number.parseInt(priceString)
if (!Number.isFinite(price) || price <= 0) {
throw new Error(`Invalid price "${priceString}"`)
}
const { userbank } =
await audiusLibs.solanaWeb3Manager.createUserBankIfNeeded({
mint: 'usdc'
})
return {
usdc_purchase: {
price,
splits: { [userbank.toString()]: price * 10 ** 4 }
}
}
}
return null
}
2 changes: 1 addition & 1 deletion packages/commands/src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import './tip-audio.mjs'
import './auth-headers.mjs'
import './get-audio-balance.mjs'
import './create-user-bank.mjs'
import './purchase-track.mjs'
import './purchase-content.mjs'
import './route-tokens-to-user-bank.mjs'
import './withdraw-tokens.mjs'

Expand Down
81 changes: 81 additions & 0 deletions packages/commands/src/purchase-content.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import chalk from 'chalk'
import { program } from 'commander'

import { initializeAudiusLibs } from './utils.mjs'

program
.command('purchase-content')
.description('Purchases a track or album using USDC')
.argument('<id>', 'The track_id or playlist_id to purchase')
.option('-f, --from [from]', 'The account purchasing the content (handle)')
.option('-t, --type [type]', 'The content type to purchase (album or track)')
.option(
'-e, --extra-amount [amount]',
'Extra amount to pay in addition to the price (in cents)'
)
.action(async (contentId, { from, type, extraAmount: extraAmountCents }) => {
type = type || 'track'
const audiusLibs = await initializeAudiusLibs(from)
const user = audiusLibs.userStateManager.getCurrentUser()

let blocknumber
let streamConditions
if (type === 'track') {
const track = (await audiusLibs.Track.getTracks(100, 0, [contentId]))[0]
if (!track.stream_conditions || !track.is_stream_gated) {
program.error('Track is not stream gated')
}
if (!track.stream_conditions?.usdc_purchase?.splits) {
program.error('Track is not purchaseable')
}
blocknumber = track.blocknumber
streamConditions = track.stream_conditions
} else if (type === 'album') {
const album = (
await audiusLibs.Playlist.getPlaylists(100, 0, [contentId])
)[0]
if (!album.is_album) {
program.error('Playlist is not an album')
}
if (!album.stream_conditions || !album.is_stream_gated) {
program.error('Album is not stream gated')
}
if (!album.stream_conditions?.usdc_purchase?.splits) {
program.error('Album is not purchaseable')
}
blocknumber = album.blocknumber
streamConditions = album.stream_conditions
} else {
program.error('Invalid type')
}

let extraAmount
if (extraAmountCents) {
const parsedExtraAmount = Number.parseInt(extraAmountCents)
if (!Number.isFinite(parsedExtraAmount) || parsedExtraAmount <= 0) {
program.error(`Invalid extra amount: ${extraAmountCents}`)
}
extraAmount = parsedExtraAmount * 10 ** 4
}

try {
const response = await audiusLibs.solanaWeb3Manager.purchaseContent({
id: contentId,
extraAmount,
type,
blocknumber,
splits: streamConditions.usdc_purchase.splits,
purchaserUserId: user.user_id,
purchaseAccess: 'stream'
})
if (response.error) {
program.error(chalk.red(response.error))
}
console.log(chalk.green(`Successfully purchased ${type}`))
console.log(chalk.yellow('Transaction Signature:'), response.res)
} catch (err) {
program.error(err.message)
}

process.exit(0)
})
55 changes: 0 additions & 55 deletions packages/commands/src/purchase-track.mjs

This file was deleted.

2 changes: 2 additions & 0 deletions packages/common/src/api/purchases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const purchasesApi = createApi({
context
)
}
// TODO: Purchaseable Albums - fetch metadata for albums
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually like to attach linear IDs/URLs to these so they don't get lost.

return purchases
},
options: { retry: true }
Expand Down Expand Up @@ -145,6 +146,7 @@ const purchasesApi = createApi({
context
)
}
// TODO: Purchaseable Albums - fetch metadata for albums
return purchases
},
options: { retry: true }
Expand Down
3 changes: 2 additions & 1 deletion packages/common/src/models/USDCTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export enum USDCTransactionMethod {
}

export enum USDCContentPurchaseType {
TRACK = 'track'
TRACK = 'track',
ALBUM = 'album'
}

export type USDCPurchaseDetails = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS album_price_history (
playlist_id integer NOT NULL,
splits JSONB NOT NULL, -- Represents amounts per each Solana account
total_price_cents bigint NOT NULL,
blocknumber integer NOT NULL,
block_timestamp timestamp WITHOUT TIME ZONE NOT NULL,
created_at timestamp WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (playlist_id, block_timestamp),
CONSTRAINT blocknumber_fkey FOREIGN KEY (blocknumber) REFERENCES blocks("number")
);
Loading