from abc import ABC, abstractmethod from datetime import date, timedelta from typing import Any, Dict, List from src.legacy.core.vendor.base import VendorBase class VendorPlaylists(VendorBase, ABC): @abstractmethod def playlists_search_handler(self, query: str, size: int, *args, **kwargs) -> List[dict]: pass @abstractmethod def get_playlists_markets(self, playlist_ids: List[str], *args, **kwargs) -> Dict[str, dict]: pass def search_across_tracks_in_playlist( self, tracks: List[Dict], search: str or None, source_isrc: str or None = None ) -> List[Dict]: """Filter list of tracks by name and artist name fields. And also implement logic for source track adding to search result. Args: tracks (List[Dict]: List of playlist's tracks. search (str or None): Search string to filter by. source_isrc (str or None): Source track ISRC value. Returns: List[Dict]: List of tracks with founded items. """ if not search: return tracks source_track = ( next((track for track in tracks if track["isrc"] and track["isrc"].lower() == source_isrc.lower()), None) if source_isrc else None ) items = list(filter(lambda track: self.filter_playlist_tracklist(track, search), tracks)) if not items or source_track in items: return items return items + [source_track] if source_track else items def filter_playlist_tracklist(self, track: Dict, search: str) -> bool: """Method that check all playlist's tracks filtering condition. Args: track (Dict): Dictionary with all required track data. search (str): Search string. Returns: bool: True - track should be included in result list, False - otherwise. """ name_match = track.get("name") and search in track["name"].lower() artists_match = search in ",".join([a for a in track.get("artists", []) if a]).lower() return name_match or artists_match def show_recent_adds( self, items: List[dict], recent_adds_only: bool, countdown_date: date or None = None ) -> List[dict]: """Filter list of objects by added_date field, showing only items after specific date. Args: items (List[str]): List of objects. recent_adds_only (bool): Filter by added_date or not. countdown_date (date or None): Date to filter by. Returns: List[dict]: Filtered (or not) list of objects. """ if not recent_adds_only: return items # if last date is not passed - set default one to -1 week from today. _date = countdown_date if countdown_date else date.today() - timedelta(weeks=1) return list(filter(lambda item: item.get("added_date") and item["added_date"] > _date, items)) def show_recent_removals( self, items: List[dict], recent_removals_only: bool = True, countdown_date: date or None = None ) -> List[dict]: """Filter list of objects by removed_date field, showing only items after specific date. Args: items (List[str]): List of objects. recent_removals_only (bool): Filter by removed_date or not. countdown_date (date or None): Date to filter by. Returns: List[dict]: Filtered (or not) list of objects. """ if not recent_removals_only: return items # if last date is not passed - set default one to -1 week from today. _date = countdown_date if countdown_date else date.today() - timedelta(weeks=1) return list(filter(lambda item: item.get("removed_date") and item["removed_date"] > _date, items)) def get_playlists_search_es_body( self, base_query: str, factor_field: str, field_name: str = "name", size: int = 20 ) -> dict: """Builds and returns Elasticsearch (ES) request body for playlists search functionality. Args: base_query (str): Search string. factor_field (str): ES function factor field name. field_name (str): ES source field for querying in. size (int): Num of items in ES response. Returns: dict: ES request body. """ split_query = base_query.split() es_should_queries = [ { # wildcard query "query_string": { "query": f"{'* AND '.join(split_query)}*", "default_field": field_name, "boost": 2, "analyze_wildcard": True, } }, { # terms query "query_string": {"query": f"{' AND '.join(split_query)}", "default_field": field_name, "boost": 10} }, { # fuzzy query "query_string": { "query": f"{'~ AND '.join(split_query)}~", "default_field": field_name, "fuzziness": "auto", } }, { # fuzzy wildcard query "query_string": { "query": f"{'*~ AND '.join(split_query)}*~", "default_field": field_name, "fuzziness": "auto", } }, ] if len(split_query) > 1: es_should_queries.append( { # fuzzy wildcard trailing query "query_string": { "query": f"{'~ AND '.join(split_query)}*~", "default_field": field_name, "fuzziness": "auto", } } ) body = { "query": { "function_score": { "query": {"bool": {"should": es_should_queries}}, "field_value_factor": {"field": factor_field, "modifier": "log1p"}, } }, "size": size, } return body def get_playlists_categories(self, track_playlists_data: List[Dict]) -> List[Any]: """Get list of cateogries our of playlists results. Args: track_playlists_data (List[Dict]): Playlists data. Returns: Categories """ playlist_categories = dict() for isrc_item in track_playlists_data: for playlist in isrc_item["data"]: if playlist["category_id"] and not playlist_categories.get(playlist["category_id"]): playlist_categories[playlist["category_id"]] = playlist["category_name"] playlist_categories = [{"category_id": k, "category_name": v} for k, v in playlist_categories.items()] return playlist_categories