from collections.abc import Sequence from dataclasses import dataclass from typing import Any, ClassVar, get_args from anydi import singleton from cachelib import BaseCache as Cache from fansifter_common.adapters.db.utils import normalize_order_by from fansifter_common.auth.requests import AuthAccountFilterRequest from fansifter_common.auth.services import AuthService from fansifter_common.auth.types import Permission from fansifter_common.utils.cache import cached from dmp.adapters.ows_socials import ( ArtistSocialsStats, ArtistSocialsStatsArgs, OwsSocialsClient, ) from dmp.rosters.dtos import FanDataListCriteria, FanDataListDetail from dmp.rosters.repositories import FanDataListRepository from dmp.rosters.services import GlobalFanDataAccessService from dmp.rosters.types import FanDataListOrderBy, FanDataListSocialsStatsOrderBy @dataclass(kw_only=True) class GetRostersRequest(AuthAccountFilterRequest): DEFAULT_LIMIT: ClassVar[int] = 50 DEFAULT_OFFSET: ClassVar[int] = 0 DEFAULT_ORDER_BY: ClassVar[list[FanDataListOrderBy]] = ["fansCount.desc"] countries: list[str] | None = None search: str | None = None exclude_local_reps: bool | None = None order_by: list[FanDataListOrderBy] limit: int offset: int @dataclass(frozen=True) class GetRostersResponse: total: int items: Sequence[FanDataListDetail] @singleton class GetRostersHandler: permission = Permission("roster", "view") def __init__( self, *, auth_service: AuthService, fandata_list_repository: FanDataListRepository, global_fandata_access_service: GlobalFanDataAccessService, ows_socials_client: OwsSocialsClient, cache: Cache, ) -> None: self.auth_service = auth_service self.fandata_list_repository = fandata_list_repository self.global_fandata_access_service = global_fandata_access_service self.ows_socials_client = ows_socials_client self.cache = cache def handle(self, request: GetRostersRequest) -> GetRostersResponse: account_access = self.auth_service.authorize_for_permission( request.identity_id, permission=self.permission, ) vendor_ids = account_access.filter_vendor_ids(request.vendor_id) subaccount_ids = account_access.filter_subaccount_ids(request.subaccount_id) global_vendor_ids = ( self.global_fandata_access_service.get_enabled_for_any_vendor( vendor_ids=vendor_ids, ) ) criteria = FanDataListCriteria( vendor_ids=vendor_ids, subaccount_ids=subaccount_ids, search=request.search, countries=request.countries, global_vendor_ids=global_vendor_ids, exclude_local_reps=request.exclude_local_reps, ) total = self.fandata_list_repository.count_by_criteria(criteria) if total == 0: return GetRostersResponse(total=total, items=[]) if self._has_socials_stats_order_by(request.order_by): all_fandata_lists = self.fandata_list_repository.find_by_criteria(criteria) chartmetric_artist_ids = sorted( [ item.chartmetric_artist_id for item in all_fandata_lists if item.chartmetric_artist_id ] ) socials_stats = self._get_artists_socials_stats(chartmetric_artist_ids) self._fill_social_data(all_fandata_lists, socials_stats) fandata_lists = self._sort_fandata_list(all_fandata_lists, request.order_by) fandata_lists = fandata_lists[ request.offset : request.offset + request.limit ] else: fandata_lists = self.fandata_list_repository.find_by_criteria( criteria, limit=request.limit, offset=request.offset, order_by=request.order_by, ) chartmetric_artist_ids = sorted( [ item.chartmetric_artist_id for item in fandata_lists if item.chartmetric_artist_id ] ) socials_stats = self._get_artists_socials_stats(chartmetric_artist_ids) self._fill_social_data(fandata_lists, socials_stats) return GetRostersResponse(total=total, items=fandata_lists) @cached("socials-stats") def _get_artists_socials_stats( self, chartmetric_artists_ids: list[int] ) -> dict[int, ArtistSocialsStats]: socials_stats = self.ows_socials_client.get_stats_by_artists_ids( args=ArtistSocialsStatsArgs(artists_ids=chartmetric_artists_ids) ) data = { artist_stats.chartmetric_artist_id: artist_stats for artist_stats in socials_stats } return data @staticmethod def _fill_social_data( artists: list[FanDataListDetail], social_data: dict[int, ArtistSocialsStats], ) -> None: for artist in artists: if not artist.chartmetric_artist_id: continue artist_social_data = social_data.get(artist.chartmetric_artist_id) if not artist_social_data: continue artist.spotify_monthly_listeners = ( artist_social_data.spotify_monthly_listeners ) artist.instagram_followers = artist_social_data.instagram_followers artist.tiktok_followers = artist_social_data.tiktok_followers artist.spotify_followers = artist_social_data.spotify_followers artist.facebook_followers = artist_social_data.facebook_followers artist.youtube_followers = artist_social_data.youtube_followers artist.twitter_followers = artist_social_data.twitter_followers artist.soundcloud_followers = artist_social_data.soundcloud_followers artist.deezer_followers = artist_social_data.deezer_followers @staticmethod def _sort_fandata_list( artists: Sequence[FanDataListDetail], order_by: list[FanDataListOrderBy], ) -> Sequence[FanDataListDetail]: fields = [] for sort_field in order_by: field, direction, _ = normalize_order_by(sort_field) fields.append((field, direction != "ASC")) def get_sort_field_value( model: FanDataListDetail, field: str, reverse: bool ) -> Any: value = getattr(model, field) if value is None: return float("inf") return -value if reverse else value def sorter(model: FanDataListDetail) -> tuple[Any, ...]: return tuple( get_sort_field_value(model, field, reverse) for field, reverse in fields ) return sorted(artists, key=sorter) @staticmethod def _has_socials_stats_order_by(order_by: list[FanDataListOrderBy]) -> bool: return bool( set(get_args(FanDataListSocialsStatsOrderBy)).intersection(set(order_by)) )