""" this script creates a feed.xml file based on JSON data of a podcast from apple podcast (itunes) API """ import json from pathlib import Path from datetime import datetime, timezone from email.utils import format_datetime from urllib.parse import urlparse import xml.etree.ElementTree as ET from xml.dom import minidom # sample curl for season episodes details: # "id": "1573565122-8", # "type": "podcast-seasons", # sample curl for channel details # "type": "podcasts", # "href": "/v1/catalog/us/podcasts/1573565122?l=en-US", json_path = Path('season-7-episoded-formatted.json') channel_path = Path('channel-details.json') output_path = Path('Bibliocast-custom.xml') obj = json.loads(json_path.read_text()) seasons = obj.get('data', []) channel_obj = json.loads(channel_path.read_text()) if channel_path.exists() else {} channel_data_list = channel_obj.get('data') or [] channel_attrs = channel_data_list[0].get('attributes', {}) if channel_data_list else {} episodes = [] for season in seasons: episodes.extend(season.get('relationships', {}).get('episodes', {}).get('data', [])) # Sort by releaseDateTime descending (latest first) def parse_dt(value): if not value: return None # ISO 8601 with Z if value.endswith('Z'): value = value.replace('Z', '+00:00') try: return datetime.fromisoformat(value) except ValueError: return None episodes_sorted = sorted( episodes, key=lambda ep: parse_dt(ep.get('attributes', {}).get('releaseDateTime')) or datetime.min.replace(tzinfo=timezone.utc), reverse=True, ) # Channel-level data (from channel details, fallback to episode data) first_attr = episodes_sorted[0]['attributes'] if episodes_sorted else {} channel_title = channel_attrs.get('name') or 'Bibliocast' artist_name = channel_attrs.get('artistName') or first_attr.get('artistName') artwork_url = (channel_attrs.get('artwork') or {}).get('url') or (first_attr.get('artwork') or {}).get('url') website_url = channel_attrs.get('websiteUrl') or first_attr.get('websiteUrl') base_link = None if website_url: parsed = urlparse(website_url) base_link = f"{parsed.scheme}://{parsed.netloc}/" if parsed.scheme and parsed.netloc else None channel_description = None desc = channel_attrs.get('description') if isinstance(desc, dict): channel_description = desc.get('standard') or desc.get('short') elif isinstance(desc, str): channel_description = desc if not channel_description: channel_description = 'Custom feed generated from Bibliocast episode data.' copyright_text = channel_attrs.get('copyright') or first_attr.get('copyright') # genre genres = set(channel_attrs.get('genreNames') or []) explicit_values = set() for ep in episodes_sorted: attrs = ep.get('attributes', {}) for g in (attrs.get('genreNames') or []): if g: genres.add(g) rating = (attrs.get('contentRating') or '').strip().lower() if rating: explicit_values.add(rating) channel_explicit = None channel_rating = channel_attrs.get('contentRating') if channel_rating: channel_rating = channel_rating.strip().lower() if channel_rating == 'explicit': channel_explicit = 'yes' elif channel_rating == 'clean': channel_explicit = 'clean' elif channel_rating == 'notexplicit': channel_explicit = 'no' if not channel_explicit: if 'explicit' in explicit_values: channel_explicit = 'yes' elif 'clean' in explicit_values: channel_explicit = 'clean' channel_release_dt = parse_dt(channel_attrs.get('releaseDateTime')) # Namespaces itunes_ns = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ET.register_namespace('itunes', itunes_ns) rss = ET.Element('rss', {'version': '2.0'}) channel = ET.SubElement(rss, 'channel') # Required/standard channel elements ET.SubElement(channel, 'title').text = channel_title if website_url: ET.SubElement(channel, 'link').text = website_url elif base_link: ET.SubElement(channel, 'link').text = base_link if channel_description: ET.SubElement(channel, 'description').text = channel_description if copyright_text: ET.SubElement(channel, 'copyright').text = copyright_text if channel_release_dt: ET.SubElement(channel, 'lastBuildDate').text = format_datetime(channel_release_dt) elif episodes_sorted: latest_dt = parse_dt(episodes_sorted[0].get('attributes', {}).get('releaseDateTime')) if latest_dt: ET.SubElement(channel, 'lastBuildDate').text = format_datetime(latest_dt) # iTunes channel elements if artist_name: ET.SubElement(channel, f'{{{itunes_ns}}}author').text = artist_name if channel_explicit: ET.SubElement(channel, f'{{{itunes_ns}}}explicit').text = channel_explicit if artwork_url: ET.SubElement(channel, f'{{{itunes_ns}}}image', {'href': artwork_url}) # iTunes category (use first genre, if any) if genres: for genre in sorted(genres): ET.SubElement(channel, f'{{{itunes_ns}}}category', {'text': genre}) # Helpers def duration_to_itunes(ms): if ms is None: return None try: total_seconds = int(round(ms / 1000)) except Exception: return None hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 seconds = total_seconds % 60 if hours > 0: return f"{hours}:{minutes:02d}:{seconds:02d}" return f"{minutes}:{seconds:02d}" def rating_to_itunes(value): if not value: return None value = value.strip().lower() if value == 'explicit': return 'yes' if value == 'clean': return 'clean' if value == 'notexplicit': return 'no' return None for ep in episodes_sorted: attrs = ep.get('attributes', {}) item = ET.SubElement(channel, 'item') title = attrs.get('itunesTitle') or attrs.get('name') if title: ET.SubElement(item, 'title').text = title ET.SubElement(item, f'{{{itunes_ns}}}title').text = title link = attrs.get('websiteUrl') or attrs.get('url') if link: ET.SubElement(item, 'link').text = link guid = attrs.get('guid') if guid: guid_el = ET.SubElement(item, 'guid', {'isPermaLink': 'false'}) guid_el.text = guid # Description desc = (attrs.get('description') or {}).get('standard') or (attrs.get('description') or {}).get('short') if desc: ET.SubElement(item, 'description').text = desc ET.SubElement(item, f'{{{itunes_ns}}}summary').text = desc short_desc = (attrs.get('description') or {}).get('short') if short_desc: ET.SubElement(item, f'{{{itunes_ns}}}subtitle').text = short_desc # Publication date release = attrs.get('releaseDateTime') release_dt = parse_dt(release) if release_dt: ET.SubElement(item, 'pubDate').text = format_datetime(release_dt) # Enclosure asset_url = attrs.get('assetUrl') if asset_url: ET.SubElement(item, 'enclosure', {'url': asset_url, 'type': 'audio/mpeg'}) # iTunes fields if artist_name: ET.SubElement(item, f'{{{itunes_ns}}}author').text = artist_name season_number = attrs.get('seasonNumber') if season_number is not None: ET.SubElement(item, f'{{{itunes_ns}}}season').text = str(season_number) episode_number = attrs.get('episodeNumber') if episode_number is not None: ET.SubElement(item, f'{{{itunes_ns}}}episode').text = str(episode_number) duration = duration_to_itunes(attrs.get('durationInMilliseconds')) if duration: ET.SubElement(item, f'{{{itunes_ns}}}duration').text = duration rating = rating_to_itunes(attrs.get('contentRating')) if rating: ET.SubElement(item, f'{{{itunes_ns}}}explicit').text = rating art = (attrs.get('artwork') or {}).get('url') if art: ET.SubElement(item, f'{{{itunes_ns}}}image', {'href': art}) # Pretty print rough_string = ET.tostring(rss, encoding='utf-8') parsed = minidom.parseString(rough_string) pretty_xml = parsed.toprettyxml(indent=' ', encoding='utf-8') output_path.write_bytes(pretty_xml) print(f'Wrote {output_path} with {len(episodes_sorted)} episodes')