Skip to content

Commit

Permalink
Scheduled releases poc (#6513)
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacsolo authored Nov 6, 2023
1 parent 61f2bcd commit cb57235
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 38 deletions.
8 changes: 6 additions & 2 deletions packages/common/src/hooks/useAccessAndRemixSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type UseAccessAndRemixSettingsProps = {
isRemix: boolean
initialPremiumConditions: Nullable<PremiumConditions>
isInitiallyUnlisted: boolean
isScheduledRelease: boolean
}

/**
Expand All @@ -33,7 +34,8 @@ export const useAccessAndRemixSettings = ({
isUpload,
isRemix,
initialPremiumConditions,
isInitiallyUnlisted
isInitiallyUnlisted,
isScheduledRelease
}: UseAccessAndRemixSettingsProps) => {
const hasNoCollectibles = useSelector((state: CommonState) => {
const { ethCollectionMap, solCollectionMap } =
Expand Down Expand Up @@ -83,7 +85,9 @@ export const useAccessAndRemixSettings = ({
isInitiallySpecialAccess ||
hasNoCollectibles
const noCollectibleGateFields =
noCollectibleGate || (!isUpload && !isInitiallyHidden)
noCollectibleGate ||
(!isUpload && !isInitiallyHidden) ||
!!isScheduledRelease

const noHidden = !isUpload && !isInitiallyUnlisted

Expand Down
6 changes: 4 additions & 2 deletions packages/common/src/services/remote-config/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export enum FeatureFlags {
SIGN_UP_REDESIGN = 'sign_up_redesign',
FEATURE_FLAG_ACCESS = 'feature_flag_access',
BUY_USDC_VIA_SOL = 'buy_usdc_via_sol',
IOS_USDC_PURCHASE_ENABLED = 'ios_usdc_purchase_enabled'
IOS_USDC_PURCHASE_ENABLED = 'ios_usdc_purchase_enabled',
SCHEDULED_RELEASES = 'scheduled_releases'
}

type FlagDefaults = Record<FeatureFlags, boolean>
Expand Down Expand Up @@ -122,5 +123,6 @@ export const flagDefaults: FlagDefaults = {
[FeatureFlags.SIGN_UP_REDESIGN]: false,
[FeatureFlags.FEATURE_FLAG_ACCESS]: false,
[FeatureFlags.BUY_USDC_VIA_SOL]: false,
[FeatureFlags.IOS_USDC_PURCHASE_ENABLED]: true
[FeatureFlags.IOS_USDC_PURCHASE_ENABLED]: true,
[FeatureFlags.SCHEDULED_RELEASES]: false
}
9 changes: 6 additions & 3 deletions packages/discovery-provider/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from src import api_helpers, exceptions, tracer
from src.api.v1 import api as api_v1
from src.api.v1.playlists import playlist_stream_bp
from src.api.v1.root_path import bp as root_path_bp
from src.challenges.challenge_event_bus import setup_challenge_bus
from src.challenges.create_new_challenges import create_new_challenges
from src.database_task import DatabaseTask
Expand Down Expand Up @@ -276,7 +275,6 @@ def default(self, o):
app.register_blueprint(api_v1.bp)
app.register_blueprint(api_v1.bp_full)
app.register_blueprint(playlist_stream_bp)
app.register_blueprint(root_path_bp)

return app

Expand Down Expand Up @@ -325,6 +323,7 @@ def configure_celery(celery, test_config=None):
"src.tasks.cache_current_nodes",
"src.tasks.update_aggregates",
"src.tasks.cache_entity_counts",
"src.tasks.publish_scheduled_releases"
],
beat_schedule={
"aggregate_metrics": {
Expand Down Expand Up @@ -435,6 +434,10 @@ def configure_celery(celery, test_config=None):
"task": "index_latest_block",
"schedule": timedelta(seconds=5),
},
"publish_scheduled_releases": {
"task": "publish_scheduled_releases",
"schedule": timedelta(minutes=1),
},
},
task_serializer="json",
accept_content=["json"],
Expand Down Expand Up @@ -492,7 +495,7 @@ def configure_celery(celery, test_config=None):
redis_inst.delete(INDEX_REACTIONS_LOCK)
redis_inst.delete(UPDATE_DELIST_STATUSES_LOCK)
redis_inst.delete("update_aggregates_lock")

redis_inst.delete("publish_scheduled_releases")
# delete cached final_poa_block in case it has changed
redis_inst.delete(final_poa_block_redis_key)

Expand Down
102 changes: 102 additions & 0 deletions packages/discovery-provider/src/tasks/publish_scheduled_releases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from datetime import datetime, timedelta
from datetime import datetime

from src.models.tracks.track import Track

from src.tasks.celery_app import celery
from src.utils.structured_logger import StructuredLogger, log_duration
from src.utils.web3_provider import get_eth_web3

logger = StructuredLogger(__name__)
web3 = get_eth_web3()
publish_scheduled_releases_cursor_key = "publish_scheduled_releases_cursor"
batch_size = 1000


def convert_timestamp(release_date_str):
parts = release_date_str.split(" ")
time_zone_offset = parts[-1] # Should be "GMT-0700" in your example

# Create a datetime object without the time zone offset
date_str_no_offset = " ".join(parts[:-1])
date_time = datetime.strptime(date_str_no_offset, "%a %b %d %Y %H:%M:%S")

# Extract the offset values (hours and minutes)
hours_offset = int(time_zone_offset[4:6])
minutes_offset = int(time_zone_offset[6:])

# Calculate the time zone offset as a timedelta
offset = timedelta(hours=hours_offset, minutes=minutes_offset)

# Adjust the datetime using the offset
adjusted_datetime = date_time - offset

# Convert the adjusted datetime to an epoch timestamp
epoch_timestamp = int(adjusted_datetime.timestamp())
return epoch_timestamp


@log_duration(logger)
def _publish_scheduled_releases(session, redis):
latest_block = web3.eth.get_block("latest")
current_timestamp = latest_block.timestamp
previous_cursor = redis.get(publish_scheduled_releases_cursor_key)
if not previous_cursor:
previous_cursor = datetime.min

candidate_tracks = (
session.query(Track)
.filter(
Track.is_unlisted,
Track.release_date.isnot(None), # Filter for non-null release_date
Track.created_at >= previous_cursor,
)
.order_by(Track.created_at.asc())
.limit(batch_size)
.all()
)
# convert release date to utc
for candidate_track in candidate_tracks:
unix_time = convert_timestamp(candidate_track.release_date)
release_date_day = datetime.fromtimestamp(unix_time).date()
candidate_created_at_day = candidate_track.created_at.date()

if (
current_timestamp >= unix_time
and release_date_day > candidate_created_at_day
):
candidate_track.is_unlisted = False

if candidate_tracks:
redis.set(
publish_scheduled_releases_cursor_key, candidate_tracks[-1].created_at
)
return


# ####### CELERY TASKS ####### #
@celery.task(name="publish_scheduled_releases", bind=True)
def publish_scheduled_releases(self):
redis = publish_scheduled_releases.redis
db = publish_scheduled_releases.db

# Define lock acquired boolean
have_lock = False
# Define redis lock object
update_lock = redis.lock(
"publish_scheduled_releases_lock", blocking_timeout=25, timeout=600
)
try:
have_lock = update_lock.acquire(blocking=False)
if have_lock:
with db.scoped_session() as session:
_publish_scheduled_releases(session, redis)

else:
logger.info("Failed to acquire lock")
except Exception as e:
logger.error(f"ERROR caching node info {e}")
raise e
finally:
if have_lock:
update_lock.release()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isPremiumContentUSDCPurchaseGated
} from '@audius/common'
import { useField } from 'formik'
import moment from 'moment'

import type { ContextualMenuProps } from 'app/components/core'
import { ContextualMenu } from 'app/components/core'
Expand Down Expand Up @@ -50,6 +51,9 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
const [{ value: isUnlisted }] = useField<boolean>('is_unlisted')
const [{ value: fieldVisibility }] =
useField<FieldVisibility>('field_visibility')
const [{ value: releaseDate }] = useField<Nullable<string>>('release_date')
const isScheduledRelease =
releaseDate === null ? false : moment(releaseDate).isAfter(moment())

const fieldVisibilityLabels = fieldVisibilityKeys
.filter((visibilityKey) => fieldVisibility[visibilityKey])
Expand All @@ -69,7 +73,7 @@ export const AccessAndSaleField = (props: AccessAndSaleFieldProps) => {
if (isPremiumContentTipGated(premiumConditions)) {
return [messages.specialAccess, messages.supportersOnly]
}
if (isUnlisted) {
if (isUnlisted || isScheduledRelease) {
return [messages.hidden, ...fieldVisibilityLabels]
}
return [messages.public]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, useMemo, useRef, useState } from 'react'

import type { Nullable } from '@audius/common'
import { Theme } from '@audius/common'
import { FeatureFlags, Theme } from '@audius/common'
import { useField } from 'formik'
import moment from 'moment'
import { TouchableOpacity } from 'react-native-gesture-handler'
Expand All @@ -12,6 +12,7 @@ import type {
import DateTimePickerModal from 'react-native-modal-datetime-picker'

import { Button, Pill, Text } from 'app/components/core'
import { useFeatureFlag } from 'app/hooks/useRemoteConfig'
import { makeStyles } from 'app/styles'
import { useThemeColors, useThemeVariant } from 'app/utils/theme'

Expand Down Expand Up @@ -73,6 +74,9 @@ export const ReleaseDateField = () => {
const [isOpen, setIsOpen] = useState(false)
const { primary } = useThemeColors()
const theme = useThemeVariant()
const { isEnabled: isScheduledReleasesEnabled } = useFeatureFlag(
FeatureFlags.SCHEDULED_RELEASES
)
const maximumDate = useRef(new Date())

const releaseDate = useMemo(
Expand Down Expand Up @@ -125,7 +129,9 @@ export const ReleaseDateField = () => {
themeVariant={theme === Theme.DEFAULT ? 'light' : 'dark'}
isDarkModeEnabled={theme !== Theme.DEFAULT}
accentColor={primary}
maximumDate={maximumDate.current}
maximumDate={
isScheduledReleasesEnabled ? undefined : maximumDate.current
}
modalStyleIOS={styles.datePickerModal}
customConfirmButtonIOS={ConfirmDateButton}
customCancelButtonIOS={CancelDateButton}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useAccessAndRemixSettings
} from '@audius/common'
import { useField, useFormikContext } from 'formik'
import moment from 'moment'

import IconCaretLeft from 'app/assets/images/iconCaretLeft.svg'
import IconCart from 'app/assets/images/iconCart.svg'
Expand Down Expand Up @@ -90,6 +91,9 @@ export const AccessAndSaleScreen = () => {
const [{ value: isUnlisted }] = useField<boolean>('is_unlisted')
const [{ value: remixOf }] = useField<RemixOfField>('remix_of')
const isRemix = !!remixOf
const [{ value: releaseDate }] = useField<Nullable<string>>('release_date')
const isScheduledRelease =
releaseDate === null ? false : moment(releaseDate).isAfter(moment())

const { isEnabled: isUsdcEnabled } = useFeatureFlag(
FeatureFlags.USDC_PURCHASES
Expand All @@ -113,7 +117,7 @@ export const AccessAndSaleScreen = () => {
) {
return TrackAvailabilityType.SPECIAL_ACCESS
}
if (isUnlisted) {
if (isUnlisted || isScheduledRelease) {
return TrackAvailabilityType.HIDDEN
}
return TrackAvailabilityType.PUBLIC
Expand All @@ -132,7 +136,8 @@ export const AccessAndSaleScreen = () => {
isUpload,
isRemix,
initialPremiumConditions,
isInitiallyUnlisted: initialValues.is_unlisted
isInitiallyUnlisted: initialValues.is_unlisted,
isScheduledRelease
})

const noUsdcGate = noUsdcGateOption || !isUsdcUploadEnabled
Expand All @@ -148,35 +153,39 @@ export const AccessAndSaleScreen = () => {
)

const data: ListSelectionData[] = [
{ label: publicAvailability, value: publicAvailability },
{
label: publicAvailability,
value: publicAvailability,
disabled: isScheduledRelease
},
isUsdcEnabled
? {
label: premiumAvailability,
value: premiumAvailability,
disabled: noUsdcGate
disabled: noUsdcGate || isScheduledRelease
}
: null,
{
label: specialAccessAvailability,
value: specialAccessAvailability,
disabled: noSpecialAccessGate
disabled: noSpecialAccessGate || isScheduledRelease
},
{
label: collectibleGatedAvailability,
value: collectibleGatedAvailability,
disabled: noCollectibleGate
disabled: noCollectibleGate || isScheduledRelease
},
{
label: hiddenAvailability,
value: hiddenAvailability,
disabled: noHidden
}
].filter(removeNullable)

const items = {
[publicAvailability]: (
<PublicAvailabilityRadioField
selected={availability === TrackAvailabilityType.PUBLIC}
disabled={isScheduledRelease}
/>
)
}
Expand All @@ -185,7 +194,7 @@ export const AccessAndSaleScreen = () => {
items[premiumAvailability] = (
<PremiumRadioField
selected={availability === TrackAvailabilityType.USDC_PURCHASE}
disabled={noUsdcGate}
disabled={noUsdcGate || isScheduledRelease}
disabledContent={noUsdcGate}
previousPremiumConditions={previousPremiumConditions}
/>
Expand All @@ -195,7 +204,7 @@ export const AccessAndSaleScreen = () => {
items[specialAccessAvailability] = (
<SpecialAccessAvailability
selected={availability === TrackAvailabilityType.SPECIAL_ACCESS}
disabled={noSpecialAccessGate}
disabled={noSpecialAccessGate || isScheduledRelease}
disabledContent={noSpecialAccessGateFields}
previousPremiumConditions={previousPremiumConditions}
/>
Expand All @@ -204,7 +213,7 @@ export const AccessAndSaleScreen = () => {
items[collectibleGatedAvailability] = (
<CollectibleGatedAvailability
selected={availability === TrackAvailabilityType.COLLECTIBLE_GATED}
disabled={noCollectibleGate}
disabled={noCollectibleGate || isScheduledRelease}
disabledContent={noCollectibleGateFields}
previousPremiumConditions={previousPremiumConditions}
/>
Expand Down
Loading

0 comments on commit cb57235

Please sign in to comment.