diff --git a/packages/common/src/services/audius-api-client/AudiusAPIClient.ts b/packages/common/src/services/audius-api-client/AudiusAPIClient.ts index 9fe4f580fba..e433a93750a 100644 --- a/packages/common/src/services/audius-api-client/AudiusAPIClient.ts +++ b/packages/common/src/services/audius-api-client/AudiusAPIClient.ts @@ -1,4 +1,4 @@ -import type { AudiusLibs } from '@audius/sdk' +import type { AudiusLibs, Genre, Mood } from '@audius/sdk' import { ID, @@ -284,6 +284,9 @@ type GetSearchArgs = { limit?: number offset?: number includePurchaseable?: boolean + genre?: Genre + mood?: Mood + isVerified?: boolean } type TrendingIdsResponse = { @@ -1119,7 +1122,10 @@ export class AudiusAPIClient { kind, offset, limit, - includePurchaseable + includePurchaseable, + genre, + mood, + isVerified }: GetSearchArgs) { this._assertInitialized() const encodedUserId = encodeHashId(currentUserId) @@ -1129,7 +1135,10 @@ export class AudiusAPIClient { kind, offset, limit, - includePurchaseable + includePurchaseable, + genre, + mood, + is_verified: isVerified } const searchResponse = diff --git a/packages/common/src/store/pages/search-results/actions.ts b/packages/common/src/store/pages/search-results/actions.ts index 215058f5e63..5bbe293c211 100644 --- a/packages/common/src/store/pages/search-results/actions.ts +++ b/packages/common/src/store/pages/search-results/actions.ts @@ -1,3 +1,5 @@ +import { Genre, Mood } from '@audius/sdk' + import { SearchKind } from './types' export const FETCH_SEARCH_PAGE_RESULTS = 'SEARCH/FETCH_SEARCH_PAGE_RESULTS' @@ -15,9 +17,12 @@ export const FETCH_SEARCH_PAGE_TAGS_FAILED = export type FetchSearchPageResultsAction = { type: typeof FETCH_SEARCH_PAGE_RESULTS searchText: string - searchKind: SearchKind + kind: SearchKind limit: number offset: number + genre?: Genre + mood?: Mood + is_verified?: boolean } export type FetchSearchPageResultsSuceededAction = { @@ -58,26 +63,33 @@ export type SearchPageActions = // Query-based search -export const fetchSearchPageResults = ( - searchText: string, - searchKind: SearchKind, - limit: number, +type FetchSearchPageResultsArgs = { + searchText: string + kind: SearchKind + limit: number offset: number + genre?: Genre + mood?: Mood + isVerified?: boolean +} + +export const fetchSearchPageResults = ( + args: FetchSearchPageResultsArgs ): SearchPageActions => ({ type: FETCH_SEARCH_PAGE_RESULTS, - searchText, - searchKind, - limit, - offset + ...args }) -export const fetchSearchPageResultsSucceeded = ( - results: any, +type fetchSearchPageResultsSucceededArgs = { + results: any searchText: string +} + +export const fetchSearchPageResultsSucceeded = ( + args: fetchSearchPageResultsSucceededArgs ): SearchPageActions => ({ type: FETCH_SEARCH_PAGE_RESULTS_SUCCEEDED, - results, - searchText + ...args }) export const fetchSearchPageResultsFailed = (): SearchPageActions => ({ @@ -85,27 +97,30 @@ export const fetchSearchPageResultsFailed = (): SearchPageActions => ({ }) // Tag-based searcxh +type FetchSearchPageTagsArgs = { + tag: string + searchKind: SearchKind + limit: number + offset: number +} export const fetchSearchPageTags = ( - tag: string, - searchKind: SearchKind, - limit: number, - offset: number + args: FetchSearchPageTagsArgs ): SearchPageActions => ({ type: FETCH_SEARCH_PAGE_TAGS, - tag, - searchKind, - limit, - offset + ...args }) -export const fetchSearchPageTagsSucceeded = ( - results: any, +type FetchSearchPageTagsSucceededArgs = { + results: any tag: string +} + +export const fetchSearchPageTagsSucceeded = ( + args: FetchSearchPageTagsSucceededArgs ): SearchPageActions => ({ type: FETCH_SEARCH_PAGE_TAGS_SUCCEEDED, - results, - tag + ...args }) export const fetchSearchPageTagsFailed = (): SearchPageActions => ({ diff --git a/packages/discovery-provider/src/api/v1/helpers.py b/packages/discovery-provider/src/api/v1/helpers.py index bb2479b623f..aadffde493c 100644 --- a/packages/discovery-provider/src/api/v1/helpers.py +++ b/packages/discovery-provider/src/api/v1/helpers.py @@ -816,7 +816,7 @@ def __schema__(self): ) full_search_parser = pagination_with_current_user_parser.copy() -full_search_parser.add_argument("query", required=True, description="The search query") +full_search_parser.add_argument("query", required=False, description="The search query") full_search_parser.add_argument( "kind", required=False, @@ -838,6 +838,19 @@ def __schema__(self): type=str, description="The genres to filter by", ) +full_search_parser.add_argument( + "mood", + action="append", + required=False, + type=str, + description="The moods to filter by", +) +full_search_parser.add_argument( + "is_verified", + required=False, + type=str, + description="Only include verified users in the user results", +) verify_token_parser = reqparse.RequestParser(argument_class=DescriptiveArgument) verify_token_parser.add_argument("token", required=True, description="JWT to verify") diff --git a/packages/discovery-provider/src/api/v1/search.py b/packages/discovery-provider/src/api/v1/search.py index 3a18f58781b..809da4a5c85 100644 --- a/packages/discovery-provider/src/api/v1/search.py +++ b/packages/discovery-provider/src/api/v1/search.py @@ -46,6 +46,8 @@ def get(self): current_user_id = get_current_user_id(args) include_purchaseable = parse_bool_param(args.get("includePurchaseable")) genres = args.get("genre") + moods = args.get("mood") + is_verified = parse_bool_param(args.get("is_verified")) search_args = { "is_auto_complete": False, @@ -58,6 +60,8 @@ def get(self): "only_downloadable": False, "include_purchaseable": include_purchaseable, "genres": genres, + "moods": moods, + "only_verified": is_verified, } resp = search(search_args) return success_response(resp) diff --git a/packages/discovery-provider/src/queries/search_es.py b/packages/discovery-provider/src/queries/search_es.py index 7d5122e1cec..9b78a764d37 100644 --- a/packages/discovery-provider/src/queries/search_es.py +++ b/packages/discovery-provider/src/queries/search_es.py @@ -16,6 +16,7 @@ populate_user_metadata_es, ) from src.utils.hardcoded_data import genre_allowlist +from src.utils.hardcoded_data import moods as mood_allowlist logger = logging.getLogger(__name__) @@ -26,12 +27,19 @@ def get_capitalized_genre(genre): return lowercase_to_capitalized_genre.get(genre.lower()) +lowercase_to_capitalized_mood = {mood.lower(): mood for mood in mood_allowlist} + + +def get_capitalized_mood(mood): + return lowercase_to_capitalized_mood.get(mood.lower()) + + def search_es_full(args: dict): esclient = get_esclient() if not esclient: raise Exception("esclient is None") - search_str = args.get("query", "").strip() + search_str = (args.get("query", "") or "").strip() current_user_id = args.get("current_user_id") limit = args.get("limit", 10) offset = args.get("offset", 0) @@ -40,6 +48,8 @@ def search_es_full(args: dict): is_auto_complete = args.get("is_auto_complete") include_purchaseable = args.get("include_purchaseable", False) genres = args.get("genres", []) + moods = args.get("moods", []) + only_verified = args.get("only_verified", False) do_tracks = search_type == "all" or search_type == "tracks" do_users = search_type == "all" or search_type == "users" do_playlists = search_type == "all" or search_type == "playlists" @@ -64,6 +74,7 @@ def search_es_full(args: dict): only_downloadable=only_downloadable, include_purchaseable=include_purchaseable, genres=genres, + moods=moods, ), ] ) @@ -73,7 +84,12 @@ def search_es_full(args: dict): mdsl.extend( [ {"index": ES_USERS}, - user_dsl(search_str, current_user_id), + user_dsl( + search_str=search_str, + current_user_id=current_user_id, + must_saved=False, + only_verified=only_verified, + ), ] ) @@ -355,6 +371,7 @@ def track_dsl( only_downloadable=False, include_purchaseable=False, genres=[], + moods=[], ): dsl = { "must": [ @@ -426,8 +443,24 @@ def track_dsl( } if genres: - capitalized_genres = [get_capitalized_genre(genre) for genre in genres] - dsl["filter"].append({"terms": {"genre": capitalized_genres}}) + capitalized_genres = list( + filter( + None, + [get_capitalized_genre(genre) for genre in genres if genre is not None], + ) + ) + if capitalized_genres: + dsl["filter"].append({"terms": {"genre": capitalized_genres}}) + + if moods: + capitalized_moods = list( + filter( + None, [get_capitalized_mood(mood) for mood in moods if mood is not None] + ) + ) + + if capitalized_moods: + dsl["filter"].append({"terms": {"mood": capitalized_moods}}) if only_downloadable: dsl["must"].append({"term": {"downloadable": {"value": True}}}) @@ -439,7 +472,7 @@ def track_dsl( return default_function_score(dsl, "repost_count") -def user_dsl(search_str, current_user_id, must_saved=False): +def user_dsl(search_str, current_user_id, only_verified, must_saved=False): # must_search_str = search_str + " " + search_str.replace(" ", "") dsl = { "must": [ @@ -544,6 +577,9 @@ def user_dsl(search_str, current_user_id, must_saved=False): if current_user_id and must_saved: dsl["must"].append(be_followed(current_user_id)) + if only_verified: + dsl["must"].append({"term": {"is_verified": {"value": True}}}) + if current_user_id: dsl["should"].append(be_followed(current_user_id)) diff --git a/packages/discovery-provider/src/tasks/entity_manager/entities/user.py b/packages/discovery-provider/src/tasks/entity_manager/entities/user.py index 9879c0e8778..28c14e99f4b 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/entities/user.py +++ b/packages/discovery-provider/src/tasks/entity_manager/entities/user.py @@ -328,11 +328,13 @@ def update_user_metadata( challenge_event_bus = params.challenge_bus # Iterate over the user_record keys user_record_attributes = user_record.get_attributes_dict() + + env = shared_config["discprov"]["env"] for key, _ in user_record_attributes.items(): # Update the user_record when the corresponding field exists # in metadata if key in metadata: - if key in immutable_user_fields: + if key in immutable_user_fields and env != "dev": continue setattr(user_record, key, metadata[key]) diff --git a/packages/es-indexer/README.md b/packages/es-indexer/README.md index e902373991d..6d5f8854902 100644 --- a/packages/es-indexer/README.md +++ b/packages/es-indexer/README.md @@ -39,7 +39,7 @@ npm run dev --drop ## How it works -Program attempts to avoid any gaps by doing a "catchup" on boot... when complete it swithces to processing "batches" which are events collected from postgres LISTEN / NOTIFY. +Program attempts to avoid any gaps by doing a "catchup" on boot... when complete it switches to processing "batches" which are events collected from postgres LISTEN / NOTIFY. Any error or exception will cause the program to crash and be restarted by pm2. This is inspired by the erlang "let it crash" mantra, since the startup behavior is designed to get everything into a good state. diff --git a/packages/mobile/src/components/audio/RNVideoAudioPlayer.tsx b/packages/mobile/src/components/audio/RNVideoAudioPlayer.tsx index e3a90a8845f..45b6fcf0d0f 100644 --- a/packages/mobile/src/components/audio/RNVideoAudioPlayer.tsx +++ b/packages/mobile/src/components/audio/RNVideoAudioPlayer.tsx @@ -33,6 +33,7 @@ import { useDispatch, useSelector } from 'react-redux' import { usePrevious } from 'react-use' import { useFeatureFlag } from '~/hooks' import { FeatureFlags } from '~/services' +import { getUserId } from '~/store/account/selectors' import { DEFAULT_IMAGE_URL } from 'app/components/image/TrackImage' import { getImageSourceOptimistic } from 'app/hooks/useContentNodeImage' @@ -102,7 +103,7 @@ export const RNVideoAudioPlayer = () => { const counter = useSelector(getCounter) // const repeatMode = useSelector(getRepeat) // const playbackRate = useSelector(getPlaybackRate) - // const currentUserId = useSelector(getUserId) + const currentUserId = useSelector(getUserId) // const uid = useSelector(getUid) // Player behavior determines whether to preview a track or play the full track const playerBehavior = diff --git a/packages/mobile/src/screens/search-results-screen/tabs/useFetchTabResultsEffect.tsx b/packages/mobile/src/screens/search-results-screen/tabs/useFetchTabResultsEffect.tsx index 9cb20929686..d04725427c1 100644 --- a/packages/mobile/src/screens/search-results-screen/tabs/useFetchTabResultsEffect.tsx +++ b/packages/mobile/src/screens/search-results-screen/tabs/useFetchTabResultsEffect.tsx @@ -47,12 +47,12 @@ export const useFetchTabResultsEffect = (searchKind: SearchKind) => { if (isTagSearch) { if (shouldFetch) { dispatch( - searchResultsPageActions.fetchSearchPageTags( - query, + searchResultsPageActions.fetchSearchPageTags({ + tag: query, searchKind, - ALL_CATEGORY_RESULTS_LIMIT, - 0 - ) + limit: ALL_CATEGORY_RESULTS_LIMIT, + offset: 0 + }) ) } track( @@ -65,12 +65,12 @@ export const useFetchTabResultsEffect = (searchKind: SearchKind) => { } else { if (shouldFetch) { dispatch( - searchResultsPageActions.fetchSearchPageResults( - query, - searchKind, - ALL_CATEGORY_RESULTS_LIMIT, - 0 - ) + searchResultsPageActions.fetchSearchPageResults({ + searchText: query, + kind: searchKind, + limit: ALL_CATEGORY_RESULTS_LIMIT, + offset: 0 + }) ) } track( diff --git a/packages/web/src/common/store/pages/search-page/lineups/tracks/sagas.ts b/packages/web/src/common/store/pages/search-page/lineups/tracks/sagas.ts index ff38cec4e8d..2ec7e81b882 100644 --- a/packages/web/src/common/store/pages/search-page/lineups/tracks/sagas.ts +++ b/packages/web/src/common/store/pages/search-page/lineups/tracks/sagas.ts @@ -43,13 +43,12 @@ function* getSearchPageResultsTracks({ ) results = tracks } else { - const { tracks } = yield* call( - getSearchResults, - query, - category, + const { tracks } = yield* call(getSearchResults, { + searchText: query, + kind: category, limit, offset - ) + }) results = tracks as unknown as Track[] } if (results) return results diff --git a/packages/web/src/common/store/pages/search-page/sagas.ts b/packages/web/src/common/store/pages/search-page/sagas.ts index 645d21c82e5..c40eaaa23c8 100644 --- a/packages/web/src/common/store/pages/search-page/sagas.ts +++ b/packages/web/src/common/store/pages/search-page/sagas.ts @@ -7,7 +7,8 @@ import { SearchKind, getContext } from '@audius/common/store' -import { trimToAlphaNumeric } from '@audius/common/utils' +import { Genre, trimToAlphaNumeric } from '@audius/common/utils' +import { Mood } from '@audius/sdk' import { select, call, takeLatest, put } from 'typed-redux-saga' import { processAndCacheCollections } from 'common/store/cache/collections/utils' @@ -78,7 +79,10 @@ export function* fetchSearchPageTags( : undefined } yield* put( - searchPageActions.fetchSearchPageTagsSucceeded(results, action.tag) + searchPageActions.fetchSearchPageTagsSucceeded({ + results, + tag: action.tag + }) ) if ( action.searchKind === SearchKind.TRACKS || @@ -97,12 +101,25 @@ export function* fetchSearchPageTags( } } -export function* getSearchResults( - searchText: string, - kind: SearchKind, - limit: number, +type GetSearchResultsArgs = { + searchText: string + kind: SearchKind + limit: number offset: number -) { + genre?: Genre + mood?: Mood + isVerified?: boolean +} + +export function* getSearchResults({ + searchText, + kind, + limit, + offset, + genre, + mood, + isVerified +}: GetSearchResultsArgs) { yield* waitForRead() const getFeatureEnabled = yield* getContext('getFeatureEnabled') const isUSDCEnabled = yield* call( @@ -118,7 +135,10 @@ export function* getSearchResults( kind, limit, offset, - includePurchaseable: isUSDCEnabled + includePurchaseable: isUSDCEnabled, + genre, + mood, + isVerified }) const { tracks, albums, playlists, users } = results @@ -140,49 +160,37 @@ function* fetchSearchPageResults( ) { yield* call(waitForRead) - const rawResults = yield* call( - getSearchResults, - action.searchText, - action.searchKind, - action.limit, - action.offset - ) + const { type: ignoredType, ...rest } = action + const rawResults = yield* call(getSearchResults, rest) if (rawResults) { const results = { users: - action.searchKind === SearchKind.USERS || - action.searchKind === SearchKind.ALL + action.kind === SearchKind.USERS || action.kind === SearchKind.ALL ? rawResults.users.map(({ user_id: id }) => id) : undefined, tracks: - action.searchKind === SearchKind.TRACKS || - action.searchKind === SearchKind.ALL + action.kind === SearchKind.TRACKS || action.kind === SearchKind.ALL ? rawResults.tracks.map(({ track_id: id }) => id) : undefined, albums: - action.searchKind === SearchKind.ALBUMS || - action.searchKind === SearchKind.ALL + action.kind === SearchKind.ALBUMS || action.kind === SearchKind.ALL ? rawResults.albums.map(({ playlist_id: id }) => id) : undefined, playlists: - action.searchKind === SearchKind.PLAYLISTS || - action.searchKind === SearchKind.ALL + action.kind === SearchKind.PLAYLISTS || action.kind === SearchKind.ALL ? rawResults.playlists.map(({ playlist_id: id }) => id) : undefined } yield* put( - searchPageActions.fetchSearchPageResultsSucceeded( + searchPageActions.fetchSearchPageResultsSucceeded({ results, - action.searchText - ) + searchText: action.searchText + }) ) - if ( - action.searchKind === SearchKind.TRACKS || - action.searchKind === SearchKind.ALL - ) { + if (action.kind === SearchKind.TRACKS || action.kind === SearchKind.ALL) { yield* put( tracksLineupActions.fetchLineupMetadatas(0, 10, false, { - category: action.searchKind, + category: action.kind, query: action.searchText, isTagSearch: false }) diff --git a/packages/web/src/pages/search-page-v2/SearchHeader.tsx b/packages/web/src/pages/search-page-v2/SearchHeader.tsx index 24d08348237..534935609d1 100644 --- a/packages/web/src/pages/search-page-v2/SearchHeader.tsx +++ b/packages/web/src/pages/search-page-v2/SearchHeader.tsx @@ -44,7 +44,7 @@ export const SearchHeader = (props: SearchHeaderProps) => { const { isMobile } = useMedia() - const handleChange = useCallback( + const handleCategoryChange = useCallback( (e: ChangeEvent) => { const value = e.target.value setCategory(value as CategoryKey) @@ -61,7 +61,7 @@ export const SearchHeader = (props: SearchHeaderProps) => { aria-label={'Select search category'} name='searchcategory' value={categoryKey} - onChange={handleChange} + onChange={handleCategoryChange} > {Object.entries(categories) .filter(([key]) => !isMobile || key !== 'all') diff --git a/packages/web/src/pages/search-page-v2/SearchResults.tsx b/packages/web/src/pages/search-page-v2/SearchResults.tsx index ab45e8793bb..e12d7db6902 100644 --- a/packages/web/src/pages/search-page-v2/SearchResults.tsx +++ b/packages/web/src/pages/search-page-v2/SearchResults.tsx @@ -18,6 +18,7 @@ import { import { OptionsFilterButton } from '@audius/harmony' import { Box, Flex } from '@audius/harmony/src/components/layout' import { Text } from '@audius/harmony/src/components/text' +import { Genre, Mood } from '@audius/sdk' import { css } from '@emotion/css' import { range } from 'lodash' import { useDispatch } from 'react-redux' @@ -91,15 +92,30 @@ export const SearchResults = ({ query }: SearchResultsProps) => { const playing = useSelector(getPlaying) const buffering = useSelector(getBuffering) const results = useSelector(searchResultsPageSelectors.getSearchResults) - const routeMatch = useRouteMatch<{ query: string; category: string }>( - SEARCH_PAGE - ) + const routeMatch = useRouteMatch<{ + category: string + }>(SEARCH_PAGE) + const [urlSearchParams] = useSearchParams() + const sort = urlSearchParams.get('sort') + const genre = urlSearchParams.get('genre') + const mood = urlSearchParams.get('mood') + const isVerified = urlSearchParams.get('is_verified') const isLoading = results.status === Status.LOADING const dispatch = useDispatch() useEffect(() => { - dispatch(fetchSearchPageResults(query, SearchKind.ALL, 50, 0)) - }, [dispatch, query]) + dispatch( + fetchSearchPageResults({ + searchText: query, + kind: SearchKind.ALL, + limit: 50, + offset: 0, + genre: (genre || undefined) as Genre, + mood: (mood || undefined) as Mood, + isVerified: isVerified === 'true' + }) + ) + }, [dispatch, query, sort, genre, mood, isVerified]) const isCategoryActive = useCallback( (category: Category) => routeMatch?.category === category, @@ -153,8 +169,6 @@ export const SearchResults = ({ query }: SearchResultsProps) => { const isTrackGridLayout = !isCategoryActive(Category.TRACKS) || tracksLayout === 'grid' - const [urlSearchParams] = useSearchParams() - const sort = urlSearchParams.get('sort') const updateSearchParams = useUpdateSearchParams('sort') const sortButton = ( diff --git a/packages/web/src/pages/search-page/SearchPageProvider.jsx b/packages/web/src/pages/search-page/SearchPageProvider.jsx index 2d63c4736ae..fd95284df83 100644 --- a/packages/web/src/pages/search-page/SearchPageProvider.jsx +++ b/packages/web/src/pages/search-page/SearchPageProvider.jsx @@ -90,17 +90,22 @@ class SearchPageProvider extends Component { search = (isTagSearch, query, searchKind, limit, offset) => { if (isTagSearch) { this.props.dispatch( - searchPageActions.fetchSearchPageTags(query, searchKind, limit, offset) + searchPageActions.fetchSearchPageTags({ + tag: query, + searchKind, + limit, + offset + }) ) this.props.recordTagSearch(query) } else { this.props.dispatch( - searchPageActions.fetchSearchPageResults( + searchPageActions.fetchSearchPageResults({ query, - searchKind, + kind: searchKind, limit, offset - ) + }) ) this.props.recordSearch(query) }