"""Populate show links script. run this in dev with command docker compose run --build show-links-importer --env=dev --podcast_id=={podcast_id} --spotify_secret_id={spotify_secret_id} --spotify_client_id={spotify_client_id} """ import argparse import re from urllib import parse from bs4 import BeautifulSoup import requests from selenium import webdriver from selenium.webdriver.common.by import By import spotipy from spotipy.oauth2 import SpotifyClientCredentials from podcast import config from podcast.constants import stores as podcast_stores from podcast.constants.api import REQUESTS_USER_AGENT from podcast.models import network as network_model from podcast.models import podcast as podcast_model from podcast.models import podcast_links as podcast_links_model from podcast.utils import api_utils podcast_admin_users = { 'dev': '366', 'qa': '1298', 'prod': '88' } def _get_rss_feed(podcast_slug): return 'https://feeds.megaphone.fm/{}'.format(podcast_slug) def _filter_description(description): """Filter out podcast description.""" extra_spaces = re.compile('[\\s\n\\s]+[\n\n]+') tags = re.compile('<.*?>|[\n]+|[\r]+') return tags.sub('', extra_spaces.sub(' ', description)).strip() if description else None def _get_valid_show_link(response): if response.status_code == 200: parsed_url = parse.urlparse(response.url)._replace(query='') return parsed_url.geturl() return None def _scrape_links(response, podcast_store): scraped_page = BeautifulSoup(response.content, 'html.parser') for link in scraped_page.find_all(class_='show-link'): store_name = link.img.get('alt') if store_name == podcast_store: return link.get('href') return None def _search_spotify_api(podcast_title, podcast_publisher): spotify_creds = SpotifyClientCredentials( config.SPOTIFY_CLIENT_ID, config.SPOTIFY_SECRET_ID ) spotify_client = spotipy.Spotify( client_credentials_manager=spotify_creds) return spotify_client.search( q='"{}"AND"{}"'.format(podcast_title, podcast_publisher), type='show', market='US', limit=50 ) def get_spotify_links(podcast, podcast_title): """Get spotify store link for podcast.""" publisher = podcast['host'] results = _search_spotify_api(podcast_title, publisher) for result in results['shows']['items']: if result['name'] == podcast_title \ and result['publisher'] == publisher: # match on title and publisher return result['external_urls']['spotify'] return None def get_apple_link(podcast, podcast_title): """Get apple store link for podcast.""" feed = _get_rss_feed(podcast['slug']) apple_search_url = '{}/search'.format(config.APPLE_URL) search_term = 'media=podcast&entity=podcast&attribute=titleTerm&term={}&limit=200'.format( podcast_title ) request_url = '{}?{}'.format(apple_search_url, search_term) data = requests.get(request_url).json() for result in data['results']: if result.get('feedUrl', '').lower() == feed.lower(): # match on RSS feed podcast_model.update_podcast(podcast['id'], {'apple_id': result['trackId']}) # save apple id in db return result['trackViewUrl'] # link in apple store return None def _get_podcast_title(podcast_title, env): if env == 'qa' and podcast_title: return podcast_title.replace('[PROD] ', '') return podcast_title.strip() def get_show_link_by_apple_id(apple_id, store_url): """Get store link by apple id for podcast.""" url = '{}/{}'.format(store_url, apple_id) response = requests.get(url) return _get_valid_show_link(response) def get_radio_public_show_link(podcast_slug, podcast_title, store_url): """Get radio public store link.""" rss_feed = _get_rss_feed(podcast_slug) encoded_rss_feed = parse.quote(rss_feed, safe='') encoded_url = '{}/{}'.format(store_url, encoded_rss_feed) response = requests.get(encoded_url, headers={'User-Agent': REQUESTS_USER_AGENT}) valid_url = _get_valid_show_link(response) if valid_url: scraped_page = BeautifulSoup(response.content, 'html.parser') header = scraped_page.find('h1') if header: title = header.get_text() if title == podcast_title: return valid_url return None def get_player_fm_link(podcast_slug, store_url): """Get player fm store link for podcast.""" rss_feed = _get_rss_feed(podcast_slug) encoded_feed = parse.quote(rss_feed, safe='').replace('%', '%25').replace('.', '%252E') encoded_url = '{}/{}'.format(store_url, encoded_feed) response = requests.get(encoded_url) valid_url = _get_valid_show_link(response) if valid_url: scraped_page = BeautifulSoup(response.content, 'html.parser') headers = scraped_page.find_all('h2') for header in headers: anchor_tag = header.find('a', class_='title') if anchor_tag: official_slug = anchor_tag.get('href').split('/')[-1] return '{}/{}'.format(store_url, official_slug) return None def get_stitcher_link(podcast, podcast_title): """Scrap stitcher store link for podcast.""" url = '{}/search/shows?query={}&count=10'.format( config.STITCHER_API_URL, podcast_title) results = requests.get(url).json()['data']['shows'] for result in results: filtered_podcast_description = _filter_description(podcast['description']) if filtered_podcast_description == result['description'] and \ podcast_title == result['title']: return '{}/show/{}'.format(config.STITCHER_URL, result['slug']) return None def get_deezer_link(podcast, podcast_title): """Search Deezer api for deezer link. This is currently not in use as the Deezer API appears to be failing, and we lack the credentials needed to access the API documentation. """ url = '{}/search/podcast?strict=on&q={}'.format( config.DEEZER_URL, podcast_title) results = requests.get(url).json()['data'] for result in results: if podcast_title == result['title'] and \ podcast['description'] == result['description']: return result['link'] return None def _scrape_amazon_links(podcast_title): encoded_title = parse.quote(podcast_title.replace('/', ' '), safe='') url = '{}/search/{}/podcasts?filter=IsLibrary%7Cfalse&sc=none'.format( config.AMAZON_URL, encoded_title) selenium_grid_url = 'http://{}:{}/wd/hub'.format( config.GRID_IP_NAME, config.GRID_PORT) driver = webdriver.Remote( command_executor=selenium_grid_url, options=webdriver.ChromeOptions() ) driver.implicitly_wait(10) driver.get(url) links = driver.find_elements(By.TAG_NAME, 'music-horizontal-item') link_info = [{ 'link': link.get_attribute('primary-href'), 'title': link.get_attribute('primary-text') } for link in links] driver.close() return link_info def get_amazon_link(podcast_title, env): """Scrap amazon store for podcast link.""" links = _scrape_amazon_links(podcast_title) strip_string = '[PROD] ' if env == 'QA' else ' ' for link in links: if podcast_title.strip(strip_string) == link['title'] and \ link['link'].startswith('/podcasts/'): url = link['link'] return f'{config.AMAZON_URL}{url}' return None def get_podcastaddict_show_link(podcast_slug): """Use webdriver to get podcastaddict show link.""" rss_feed = _get_rss_feed(podcast_slug) encoded_rss_feed = parse.quote(rss_feed, safe='') encoded_url = '{}/feed/{}'.format(config.PODCAST_ADDICT_URL, encoded_rss_feed) response = requests.get(encoded_url, headers={'User-Agent': REQUESTS_USER_AGENT}) return _get_valid_show_link(response) def get_pandora_link(podcast_title): """Get pandora store link for podcast.""" pandora_request_url = '{}/api/v3/sod/search'.format(config.PANDORA_PODCAST_URL) json_data = { 'query': podcast_title, 'types': ['PC', 'PE'], 'listener': None, 'start': 0, 'count': 100, 'annotate': True, 'searchTime': 0, 'annotationRecipe': 'CLASS_OF_2019' } response = requests.post(pandora_request_url, json=json_data) if response.status_code == 200: pandora_data = response.json()['annotations'] for show_data in pandora_data.values(): if show_data['name'] == podcast_title: return '{}{}'.format(config.PANDORA_PODCAST_URL, show_data['shareableUrlPath']) return None def get_link(podcast, store, apple_id, env): """Get link for podcast.""" podcast_title = _get_podcast_title(podcast['title'], env) if store['name'] == podcast_stores.APPLE_PODCAST: return get_apple_link(podcast, podcast_title) if store['name'] == podcast_stores.SPOTIFY: return get_spotify_links(podcast, podcast_title) if store['name'] == podcast_stores.STITCHER: return get_stitcher_link(podcast, podcast_title) if store['name'] == podcast_stores.CASTBOX and apple_id: return get_show_link_by_apple_id(apple_id, config.CASTBOX_URL) if store['name'] == podcast_stores.POCKET_CASTS and apple_id: return get_show_link_by_apple_id(apple_id, config.POCKET_CASTS_URL) if store['name'] == podcast_stores.RADIO_PUBLIC: return get_radio_public_show_link(podcast['slug'], podcast['title'], config.RADIO_PUBLIC_URL) if store['name'] == podcast_stores.PODCAST_ADDICT: return get_podcastaddict_show_link(podcast['slug']) if store['name'] == podcast_stores.PLAYER_FM: return get_player_fm_link(podcast['slug'], config.PLAYER_FM_URL) if store['name'] == podcast_stores.AMAZON_MUSIC: return get_amazon_link(podcast_title, env) if store['name'] == podcast_stores.PANDORA: return get_pandora_link(podcast_title) return None def get_new_store_links(podcast, stores, current_store_ids, apple_id, env): """Get links for currently missing stores.""" scraped_stores = [ podcast_stores.CASTBOX, podcast_stores.POCKET_CASTS, podcast_stores.GOOGLE_PODCAST ] new_store_links = [] for store in stores: store_id = store['id'] if store_id in current_store_ids: # podcast has this store link continue if apple_id is None and store['name'] in scraped_stores: apple_id = podcast_model.get_podcast_by_id(podcast['id']).get('apple_id') link = get_link(podcast, store, apple_id, env) if link: new_store_links.append({ 'store_id': store_id, 'link': link }) return new_store_links def populate_podcast_links(podcast, stores, env): """Populate store ids for specific podcast.""" current_links = podcast_links_model.get_podcast_links(podcast['id'])['items'] current_store_ids = [link['store_id'] for link in current_links] new_links = get_new_store_links(podcast, stores, current_store_ids, podcast.get('apple_id'), env) if len(new_links): podcast_links_model.create_podcast_links({ 'podcast_id': podcast['id'], 'links': current_links + new_links }) def populate_all_podcasts_links(env): """Fetch all podcasts and populate store ids for each of them.""" networks = network_model.get_networks()['items'] network_ids = [network['id'] for network in networks] stores = podcast_links_model.get_stores()['items'] podcasts = podcast_model.get_podcasts_by_network_ids(network_ids=network_ids)['items'] for podcast in podcasts: if podcast['slug'] is not None: populate_podcast_links(podcast, stores, env) def main(): """Extract shell arguments and populate store ids.""" argparser = argparse.ArgumentParser(prog='shows link importer') argparser.add_argument('--podcast_id', required=False, help='podcast id') argparser.add_argument('--env', required=False, help='env', default='dev') argparser.add_argument('--spotify_secret_id', required=True, help='Spotify Secret ID') argparser.add_argument('--spotify_client_id', required=True, help='Spotify Client ID') args = argparser.parse_args() podcast_id = args.podcast_id env = args.env config.SPOTIFY_CLIENT_ID = args.spotify_client_id config.SPOTIFY_SECRET_ID = args.spotify_secret_id def get_user_id(): """Mock get user id return value.""" return podcast_admin_users[env] api_utils.get_user_id = get_user_id if podcast_id: podcast = podcast_model.get_podcast_by_id(podcast_id) stores = podcast_links_model.get_stores()['items'] if podcast['slug'] is not None: populate_podcast_links(podcast, stores, env) else: populate_all_podcasts_links(env) if __name__ == '__main__': main()