]> git.sthu.org Git - oe1archive.git/commitdiff
major revision mainly to support 30-days back stream captures
authorG.Mitterlechner <gmitterlechner.lba@fh-salzburg.ac.at>
Wed, 31 Dec 2025 13:50:34 +0000 (14:50 +0100)
committerStefan Huber <shuber@sthu.org>
Fri, 2 Jan 2026 19:08:25 +0000 (20:08 +0100)
README.markdown
oe1archive

index 2c3e8e9483e657b2e8b41ae5d0002b4c85676b9c..9e4f31f4104b612cb108fc76a7ad6a5f675c275b 100644 (file)
@@ -1,19 +1,90 @@
 # oe1archive
 
-A tool do download Ö1 radio broadcasts. This repo is a clone of this [original
-repo](https://git.sthu.org/?p=oe1archive.git) and there is also a
-[website](https://www.sthu.org/code/codesnippets/oe1archive.html).
+A tool to download Ö1 radio broadcasts. This repo is a clone of this [original repo](https://git.sthu.org/?p=oe1archive.git) and there is also a [website](https://www.sthu.org/code/codesnippets/oe1archive.html).
 
-To search or fetch broadcasts with oe1archive, call it like this:
+## Overview
 
-    % ./oe1archive -h
-    Usage:
-        ./oe1archive -h, --help
-        ./oe1archive -c, --choose
-        ./oe1archive -s, --search TITLE
+Browse, search, and download OE1 (Austrian National Radio) broadcasts from the past 30 days with automatic HTML archiving.
 
-    % ./oe1archive -s jazz
+## Features
 
+- **30-day Interactive Browse** - Select any date and download directly
+- **Fast Search** - Search by title and subtitle (≈10 seconds)
+- **Deep Search** - Optional search including descriptions (≈5 minutes)
+- **Smart Stream Extraction** - Automatic loopstream ID retrieval
+- **Batch Processing** - Download multiple shows efficiently
+- **Organized Storage** - Timestamped folders with HTML metadata (format: `prefix_yyyyMMdd_hhmm`)
+
+## Requirements
+
+Python 3.6+ with dependencies:
+```
+pip install requests simplejson python-dateutil
+```
+
+## Usage
+
+### Interactive Mode
+```bash
+./oe1archive -c
+```
+Browse all 30 days and download shows directly.
+
+### Search Shows by Title and Subtitle
+```bash
+./oe1archive -s "Show Name"
+```
+Fast search across title and subtitle fields. **Takes approximately 10 seconds.** Use this for most searches.
+
+### Extended Search (including descriptions)
+```bash
+./oe1archive -s "Show Name" -e
+```
+Search all available broadcasts including full description text. **Takes approximately 5 minutes.** Use this if the regular search doesn't find what you're looking for.
+
+### Batch Download
+```bash
+./oe1archive -d "Show Name" -p "prefix"
+```
+Download all matching broadcasts with metadata and audio. Use `-e` flag for extended search:
+`./oe1archive -d "Show Name" -p "prefix" -e`
+
+### Command Examples
+
+```bash
+# Show usage
+./oe1archive -h
+
+# Fast search by title/subtitle
+./oe1archive -s "Show Name"
+
+# Deep search including descriptions
+./oe1archive -s "Some Keyword" -e
+
+# Batch download all matches (fast search)
+./oe1archive -d "Show Name" -p "prefix"
+
+# Batch download all matches (deep search)
+./oe1archive -d "Some Keyword" -p "prefix" -e
+```
+
+## How It Works
+
+The OE1 API provides a rolling weekly window of current broadcasts. Streams remain accessible via loopstream for ~30 days, enabling full month archive access through direct download without search requirements.
+
+## Technical Details
+
+- **API**: http://audioapi.orf.at/oe1/json/2.0/
+- **Stream Source**: https://loopstream01.apa.at/?channel=oe1&shoutcast=0&id=...
+- **Archive Window**: ~30 days (loopstream availability)
+
+## Output Structure
+
+```
+prefix_yyyyMMdd_hhmm/
+├── prefix_yyyyMMdd_hhmm.html    (Metadata)
+└── prefix_yyyyMMdd_hhmm.mp3     (Audio)
+```
 
 ## Contributors
 
index 253fde64bce95e7ce4d05d71f480dacc11f38ba2..023cdc78b90042af5c0856d7d021d3baddd4669c 100755 (executable)
@@ -1,9 +1,57 @@
 #!/usr/bin/env python3
 
-"""A simple tool to query the Oe1 7 Tage archive."""
-
-__version__ = "2.1"
-__author__ = "Stefan Huber"
+"""A tool to query the OE1 30-day archive and download streams with HTML pages.
+
+FEATURES:
+- Browse all 30 days in interactive mode
+- Recent week (API window): Direct show selection and download
+- Older 22+ days (archive): Search for specific shows, then download
+- Automatic loopstream ID extraction (no manual URL searching)
+- Batch download of matching shows with HTML metadata
+- Proper file organization with timestamped folders
+- Fast search on title and subtitle, optional extended search with descriptions
+
+HOW IT WORKS:
+The OE1 API provides a rolling weekly broadcast listing. Shows are accessible via
+loopstream IDs for approximately 30 days. This tool combines:
+
+1. INTERACTIVE MODE (-c):
+   - Shows all 30 days of dates
+   - Recent week: Select from full broadcast list
+   - Older dates: Directs you to search for specific shows
+
+2. SEARCH MODE (-s):
+   - Searches all available broadcasts across the 30-day window
+   - Matches against title and subtitle (fast)
+   - Returns matching shows that can be downloaded
+
+3. EXTENDED SEARCH MODE (-s -e):
+   - Searches title, subtitle, AND description (slower, ~5 minutes)
+   - Use when regular search doesn't find what you're looking for
+   - Returns matching shows that can be downloaded
+
+4. BATCH DOWNLOAD (-d + -p):
+   - Automatically downloads all matching broadcasts
+   - Creates organized folders with HTML metadata
+   - Use -e flag for extended search matching
+
+ACCESSING OLDER SHOWS:
+For dates older than 8 days, use search by show name:
+   ./oe1archive -s "Show Name"
+
+You can also manually browse: https://oe1.orf.at/programm/YYYYMMDD
+Then search for the show by name in this tool.
+
+USAGE EXAMPLES:
+  ./oe1archive -c                     # Browse and select
+  ./oe1archive -s "Some title"            # Search by title/subtitle (fast)
+  ./oe1archive -s "Some description" -e         # Extended search with description (slow)
+  ./oe1archive -d "Some title" -p "L"     # Download all matches
+  ./oe1archive -d "Some description" -p "L" -e  # Download with extended search
+"""
+
+__version__ = "3.0"
+__author__ = "Stefan Huber, Gerhard Mitterlechner"
 
 
 import urllib.request
@@ -13,238 +61,626 @@ import sys
 import getopt
 import re
 import os
+import requests
+from datetime import datetime, timedelta
+import time
+
+
+class SuperArchive:
+    """Access OE1 archive with extended 30-day capability.
 
+    The OE1 API provides a rolling weekly window of broadcasts. However, loopstream IDs
+    remain valid for approximately 30 days. This tool simulates a 30-day view by:
+    1. Fetching the current weekly API data
+    2. Creating placeholder entries for dates before the API window (up to 30 days back)
+    3. Allowing searches across all dates - when found, the actual broadcast data is used
+    """
 
-class Archive:
+    def __init__(self, days=30):
+        """Initialize archive with configurable day range (default 30 days)."""
+        self.days_back = days
+        self.api_json = self._read_archive()
+        self.json = self._generate_30day_view()
+
+    def _read_archive(self):
+        """Read the current weekly archive from the API."""
+        try:
+            json_data = read_json(
+                "http://audioapi.orf.at/oe1/json/2.0/broadcasts/")
+            print(
+                f"Loaded {len(json_data)} days from OE1 API",
+                file=sys.stderr)
+            return json_data
+        except Exception as e:
+            print(f"Error fetching broadcasts: {e}", file=sys.stderr)
+            return []
+
+    def _generate_30day_view(self):
+        """Generate a 30-day view by fetching data from the API.
+
+        Uses the path parameter API endpoint (/broadcasts/YYYYMMDD/) to fetch
+        all broadcasts for each day, extending beyond the standard weekly window.
+        """
+        extended_json = []
+
+        if not self.api_json:
+            return extended_json
+
+        # Add all API data
+        extended_json.extend(self.api_json)
+
+        # For dates older than the API window, fetch via path parameter API
+        if len(self.api_json) > 0:
+            try:
+                oldest_api_date = dateutil.parser.parse(
+                    self.api_json[-1].get('dateISO', ''))
+                print(
+                    f"Loading extended archive data (this may take a moment)...",
+                    file=sys.stderr)
+
+                # Fetch broadcasts for dates older than the API window
+                # Start from len(self.api_json) to avoid re-fetching dates
+                # already in api_json
+                for days_offset in range(
+                        len(self.api_json), self.days_back + 1):
+                    archive_date = oldest_api_date - \
+                        timedelta(days=days_offset)
+                    date_int = int(archive_date.strftime('%Y%m%d'))
+
+                    try:
+                        # Use path parameter API to get broadcasts for specific
+                        # date
+                        api_url = f"http://audioapi.orf.at/oe1/json/2.0/broadcasts/{date_int}/"
+                        broadcasts_data = read_json(api_url)
+
+                        if broadcasts_data:
+                            # Create entry for this date with fetched
+                            # broadcasts
+                            archive_entry = {
+                                'dateISO': archive_date.isoformat(),
+                                'day': date_int,
+                                'broadcasts': broadcasts_data
+                            }
+                            extended_json.append(archive_entry)
+                            print(
+                                f"  Loaded {archive_date.strftime('%a %d.%b')}: {len(broadcasts_data)} broadcasts",
+                                file=sys.stderr)
+                        else:
+                            # Fallback to guide entry if fetch fails
+                            guide_entry = {
+                                'dateISO': archive_date.isoformat(),
+                                'day': date_int,
+                                'broadcasts': [{
+                                    'title': f'[Archive Guide: {archive_date.strftime("%a, %d. %b %Y")}]',
+                                    'subtitle': 'Use -s to search for shows from this date',
+                                    'startISO': archive_date.isoformat(),
+                                    'programKey': f'archive_{date_int}',
+                                    'is_guide': True
+                                }]
+                            }
+                            extended_json.append(guide_entry)
+                    except Exception as e:
+                        # If individual date fetch fails, create guide entry
+                        guide_entry = {
+                            'dateISO': archive_date.isoformat(),
+                            'day': date_int,
+                            'broadcasts': [{
+                                'title': f'[Archive Guide: {archive_date.strftime("%a, %d. %b %Y")}]',
+                                'subtitle': 'Use -s to search for shows from this date',
+                                'startISO': archive_date.isoformat(),
+                                'programKey': f'archive_{date_int}',
+                                'is_guide': True
+                            }]
+                        }
+                        extended_json.append(guide_entry)
+
+                    # Small delay to avoid overwhelming the API
+                    time.sleep(0.1)
+
+                print(
+                    f"Archive data loaded: {len(extended_json)} days total",
+                    file=sys.stderr)
+            except Exception as e:
+                print(
+                    f"Note: Could not extend to 30 days: {e}",
+                    file=sys.stderr)
 
-    def __init__(self):
-        self.json = read_json("http://audioapi.orf.at/oe1/json/2.0/broadcasts/")
+        return extended_json
 
     def get_days(self):
-        return map(_json_to_day, self.json)
+        """Return list of available days."""
+        return list(map(_json_to_day, self.json))
 
     def get_broadcasts(self, day):
-        bjson = self.json[day]['broadcasts']
-        return map(_json_to_broadcast, bjson)
+        """Return broadcasts for a given day index."""
+        if day < 0 or day >= len(self.json):
+            return []
+        bjson = self.json[day].get('broadcasts', [])
+        # Don't filter - return all entries including guides
+        return list(map(_json_to_broadcast, bjson))
 
     def get_broadcast(self, day, broadcast):
-        return _json_to_broadcast(self.json[day]['broadcasts'][broadcast])
+        """Return specific broadcast information."""
+        if day < 0 or day >= len(self.json) or broadcast < 0:
+            return (None, None)
+        broadcasts = self.json[day].get('broadcasts', [])
+        if broadcast >= len(broadcasts):
+            return (None, None)
+        return _json_to_broadcast(broadcasts[broadcast])
 
     def get_player_url(self, day, broadcast):
+        """Get the player URL for a broadcast."""
         date = self.json[day]['day']
         pk = self.json[day]['broadcasts'][broadcast]['programKey']
         url = "http://oe1.orf.at/player/%d/%s"
         return url % (date, pk)
 
+    def get_broadcast_title(self, day, broadcast):
+        """Return broadcast title."""
+        return self.json[day]['broadcasts'][broadcast]['title']
+
     def get_broadcast_subtitle(self, day, broadcast):
+        """Return broadcast subtitle."""
         return self.json[day]['broadcasts'][broadcast]['subtitle']
 
     def get_broadcast_pk(self, day, broadcast):
+        """Return broadcast program key."""
         return self.json[day]['broadcasts'][broadcast]['programKey']
 
     def get_broadcast_url(self, day, broadcast):
+        """Get the stream URL for a broadcast.
+
+        Handles both API broadcasts and archive shows from program pages.
+        """
+        broadcast_entry = self.json[day]['broadcasts'][broadcast]
         date = self.json[day]['day']
-        pk = self.json[day]['broadcasts'][broadcast]['programKey']
+        pk = broadcast_entry['programKey']
 
         burl = 'https://audioapi.orf.at/oe1/api/json/current/broadcast/%s/%d'
-        bjson = read_json(burl % (pk, date))
+        try:
+            bjson = read_json(burl % (pk, date))
+        except Exception as e:
+            print(
+                f"Warning: Could not fetch broadcast details: {e}",
+                file=sys.stderr)
+            return None
 
-        sjson = bjson['streams']
+        sjson = bjson.get('streams', [])
         if len(sjson) == 0:
             return None
 
-        sid = sjson[0]['loopStreamId']
+        sid = sjson[0].get('loopStreamId')
+        if sid is None:
+            return None
+
         surl = 'https://loopstream01.apa.at/?channel=oe1&shoutcast=0&id=%s'
         return surl % sid
 
     def get_broadcast_description(self, day, broadcast):
+        """Get broadcast description and akm info."""
         date = self.json[day]['day']
         pk = self.json[day]['broadcasts'][broadcast]['programKey']
 
         burl = 'https://audioapi.orf.at/oe1/api/json/current/broadcast/%s/%d'
-        bjson = read_json(burl % (pk, date))
+        try:
+            bjson = read_json(burl % (pk, date))
+        except Exception as e:
+            return ""
 
-        description = bjson['description']
-        akm = bjson['akm']
+        description = bjson.get('description', "")
+        akm = bjson.get('akm', "")
         if description is None:
             description = ""
         if akm is None:
             akm = ""
-        return description + "<br>" + akm;
+        return description + "<br>" + akm
 
-    def get_broadcasts_by_regex(self, key):
+    def get_broadcasts_by_regex(self, key, deep_search=False):
+        """Find broadcasts matching a regex pattern.
+
+        Args:
+            key: Search pattern (regex)
+            deep_search: If True, search in title, subtitle, and description.
+                        If False, search only in title and subtitle (faster).
+
+        Skips placeholder entries.
+        """
         rex = re.compile(key, re.IGNORECASE)
 
         res = []
+        total_broadcasts = sum(len(djson['broadcasts']) for djson in self.json)
+        checked_broadcasts = 0
+
         for d, djson in enumerate(self.json):
             for b, bjson in enumerate(djson['broadcasts']):
+                checked_broadcasts += 1
+
+                # Show progress every 10 broadcasts
+                if checked_broadcasts % 10 == 0:
+                    print(
+                        f"Searching... {checked_broadcasts}/{total_broadcasts} broadcasts checked",
+                        file=sys.stderr)
+
+                # Skip placeholder entries in search
+                if bjson.get('is_placeholder', False):
+                    continue
+
+                found = False
+
+                # Search in title
                 if rex.search(bjson['title']) is not None:
+                    found = True
+
+                # Search in subtitle
+                if not found:
+                    subtitle = bjson.get('subtitle')
+                    if subtitle is not None and rex.search(
+                            subtitle) is not None:
+                        found = True
+
+                # Search in description (only if deep_search is enabled)
+                if not found and deep_search:
+                    try:
+                        date = djson['day']
+                        pk = bjson['programKey']
+                        burl = 'https://audioapi.orf.at/oe1/api/json/current/broadcast/%s/%d'
+                        bjson_full = read_json(burl % (pk, date))
+                        description = bjson_full.get('description', "")
+                        if description and rex.search(description) is not None:
+                            found = True
+                    except BaseException:
+                        pass
+
+                if found:
                     res.append((d, b))
-                elif bjson['subtitle'] is not None and rex.search(bjson['subtitle']) is not None:
-                    res.append((d, b))
+
+        print(f"Search complete: {len(res)} result(s) found", file=sys.stderr)
         return res
 
+    def download_broadcast(self, day, broadcast, prefix):
+        """Download a single broadcast with HTML and MP3."""
+        try:
+            date, title = self.get_broadcast(day, broadcast)
+
+            # Skip placeholder entries
+            if date is None or 'Archive' in title:
+                print(
+                    f"  ✗ This is a placeholder entry. Use search (-s) to find shows from this date.")
+                return False
+
+            url = self.get_broadcast_url(day, broadcast)
+
+            if url is None:
+                print(f"  ✗ No stream available for: {title}")
+                return False
+
+            dirname = get_directory_name(prefix, date)
+            print(f"  ↓ {title}")
+
+            # Create directory and download files
+            make_directory(prefix, date)
+
+            description = self.get_broadcast_description(day, broadcast)
+            write_html_file(prefix, date, title, description)
+            write_mp3_file(prefix, date, url)
+
+            return True
+
+        except Exception as e:
+            print(f"  ✗ Error downloading: {e}")
+            return False
+
+
 def _json_to_day(djson):
+    """Convert JSON date to datetime object."""
     return dateutil.parser.parse(djson['dateISO'])
 
+
 def _json_to_broadcast(bjson):
+    """Convert JSON broadcast to (datetime, title) tuple."""
     dt = dateutil.parser.parse(bjson['startISO'])
     return (dt, bjson['title'])
 
 
 def read_json(url):
+    """Read JSON from URL."""
     with urllib.request.urlopen(url) as f:
         dec = simplejson.JSONDecoder()
         return dec.decode(f.read())
 
+
 def input_index(prompt, li):
+    """Get valid index from user input."""
     while True:
         try:
             idx = int(input(prompt))
             if idx < 0 or idx >= len(li):
-                print("Out out range!")
+                print("Out of range!")
             else:
                 return idx
-
         except ValueError:
             print("Unknown input.")
         except EOFError:
             sys.exit(1)
 
-def screen_help():
-    print("""Usage:
-    {0} -h, --help
-    {0} -c, --choose
-    {0} -s, --search TITLE""".format(sys.argv[0]))
 
-def screen_choose():
-    a = Archive()
+def get_directory_name(name, datetime_obj):
+    """Create directory name from prefix and datetime."""
+    prefix = ""
+    if len(name) > 0:
+        prefix = name + "_"
+
+    return prefix + datetime_obj.strftime("%Y%m%d_%H%M")
+
+
+def make_directory(name, datetime_obj):
+    """Create the download subdirectory for the given name and datetime."""
+    dirname = get_directory_name(name, datetime_obj)
+    if not os.path.exists(dirname):
+        os.makedirs(dirname)
+
+
+def write_html_file(name, datetime_obj, title, description):
+    """Store broadcast description and title into an HTML file."""
+    longname = get_directory_name(name, datetime_obj)
+    filepath = os.path.join(longname, longname + ".html")
+
+    with open(filepath, 'w+', encoding='utf-8') as file:
+        file.write("<!DOCTYPE html>\n")
+        file.write("<html>\n")
+        file.write("<head>\n")
+        file.write("<title>\n")
+        file.write(
+            "%s - %s\n" %
+            (title, datetime_obj.strftime("%d.%m.%Y %H:%M")))
+        file.write("</title>\n")
+        file.write("<meta charset=\"utf-8\">\n")
+        file.write("</head>\n")
+        file.write("<body>\n")
+        file.write("<h1>%s</h1>\n" % title)
+        file.write("<p><strong>Date/Time:</strong> %s</p>\n" %
+                   datetime_obj.strftime("%d.%m.%Y %H:%M:%S"))
+        if name:
+            file.write("<p><strong>Show:</strong> %s</p>\n" % name)
+        file.write("<hr>\n")
+        file.write(description)
+        file.write("\n</body>\n")
+        file.write("</html>")
+
+
+def write_mp3_file(name, datetime_obj, url):
+    """Download and save MP3 file from URL."""
+    longname = get_directory_name(name, datetime_obj)
+    filepath = os.path.join(longname, longname + ".mp3")
+
+    print(f"      Downloading MP3...")
+    try:
+        # Use generous timeout (3600 seconds = 60 minutes) for very large MP3
+        # files
+        r = requests.get(url, stream=True, timeout=3600)
+        if r.status_code == 200:
+            total_size = int(r.headers.get('content-length', 0))
+            downloaded = 0
+
+            with open(filepath, 'wb') as f:
+                for chunk in r.iter_content(chunk_size=8192):
+                    if chunk:
+                        f.write(chunk)
+                        downloaded += len(chunk)
+                        if total_size:
+                            percent = (downloaded / total_size) * 100
+                            print(f"      Progress: {percent:.1f}%", end='\r')
+
+            print(f"      ✓ Saved: {os.path.basename(filepath)}    ")
+        else:
+            print(f"      ✗ Error: HTTP {r.status_code}")
+    except requests.exceptions.RequestException as e:
+        print(f"      ✗ Download failed: {e}")
+
 
+def screen_help():
+    """Display help information."""
+    print("""OE1 Archive - Extended 30-day downloader
+
+Usage:
+    {0} -h, --help              Show this help message
+    {0} -c, --choose            Interactive mode - choose and download
+    {0} -s, --search TITLE      Search by title and subtitle (fast, ~10 seconds)
+    {0} -s TITLE -e             Extended search including description (slow, ~5 minutes)
+    {0} -d, --download TITLE    Auto-download all matching broadcasts
+                                 (requires directory prefix via -p)
+    {0} -p, --prefix PREFIX     Directory prefix for downloads
+    {0} -e, --extended-search   Extended search (use with -s or -d)
+
+Examples:
+    {0} -c                      Choose broadcast interactively
+    {0} -s "Music"              Search title/subtitle for "Music" (fast)
+    {0} -s "Music" -e           Extended search including description (slow)
+    {0} -d "Brunch" -p "Brunch" Download all "Brunch" broadcasts
+    {0} -d "Brunch" -p "B" -e   Download using extended search
+""".format(sys.argv[0]))
+
+
+def screen_choose(archive):
+    """Interactive mode to select and download broadcasts."""
     print("Choose a date:")
-    days = list(a.get_days())
+    days = archive.get_days()
     for i, date in enumerate(days):
-        print("  [%d]  %s" % (i, date.strftime("%a %d. %b %Y")))
+        broadcasts_count = len(archive.get_broadcasts(i))
+        if broadcasts_count == 0:
+            marker = "  (No shows available)"
+        elif broadcasts_count == 1:
+            # Check if it's a guide entry
+            b = archive.get_broadcasts(i)[0]
+            if b[1].startswith('[Archive Guide'):
+                marker = "  🔍 Use search for this date"
+            else:
+                marker = f"  ({broadcasts_count} broadcast)"
+        else:
+            marker = f"  ({broadcasts_count} broadcasts)"
+        print("  [%d]  %s%s" % (i, date.strftime("%a %d. %b %Y"), marker))
     day = input_index("Date: ", days)
     chosen_datetime = days[day]
     print()
 
+    broadcasts = archive.get_broadcasts(day)
+
+    # Check if this is a guide entry
+    if broadcasts and len(broadcasts) == 1:
+        title, _ = broadcasts[0]
+        if title and title.startswith('[Archive Guide'):
+            print(f"{title}")
+            print()
+            print("This date is in the archive window (older than 8 days).")
+            print("To download shows from this date, search by show name:")
+            print()
+            print("  ./oe1archive -s \"Show Name\"")
+            print("  ./oe1archive -d \"Show Name\" -p \"prefix\"")
+            print()
+            answer = input("Would you like to search for a show? (y/N) ")
+            if answer in ["y", "Y", "j", "J"]:
+                search_term = input("Enter show name to search for: ")
+                print()
+                screen_search(archive, search_term)
+            return
+
+    # Check if date has actual broadcasts
+    if not broadcasts:
+        print(
+            f"No broadcasts available for {chosen_datetime.strftime('%A, %d. %B %Y')}.")
+        return
+
     print("Choose a broadcast:")
-    broadcasts = list(a.get_broadcasts(day))
     for i, b in enumerate(broadcasts):
         date, title = b
         print("  [%2d]  %s  %s" % (i, date.strftime("%H:%M:%S"), title))
     broadcast = input_index("Broadcast: ", broadcasts)
     print()
 
-    print_broadcast_info(a, day, broadcast)
+    print_broadcast_info(archive, day, broadcast)
     print()
 
-    url = a.get_broadcast_url(day, broadcast)
+    url = archive.get_broadcast_url(day, broadcast)
     if url is not None:
-        answer = input("Do you want to download the chosen broadcast? (y/N) ")
+        answer = input("Do you want to download this broadcast? (y/N) ")
         if answer in ["y", "Y", "j", "J"]:
-            name = input("Download directory (prefix): ")
-
-            try:
-                dirname = get_directory_name(name, chosen_datetime)
-                print("Downloading to %s..." % dirname)
+            prefix = input("Directory prefix (optional): ")
+            archive.download_broadcast(day, broadcast, prefix)
+            print("\nDownload completed!")
+    else:
+        print("No stream available for this broadcast.")
 
-                make_directory(name, chosen_datetime)
 
-                description = a.get_broadcast_description(day, broadcast)
-                write_html_file(name, chosen_datetime, description)
+def screen_search(archive, key, deep_search=False):
+    """Search for broadcasts matching a pattern.
 
-                write_mp3_file(name, chosen_datetime, url)
+    Args:
+        archive: SuperArchive instance
+        key: Search pattern
+        deep_search: If True, search in description as well (slower)
+    """
+    results = archive.get_broadcasts_by_regex(key, deep_search=deep_search)
+    if not results:
+        print(f"No broadcasts found matching: {key}")
+        return
 
-            except OSError as e:
-                print("Error creating directory.")
-                print(e)
+    print(f"Found {len(results)} broadcast(s):\n")
+    for d, b in results:
+        print_broadcast_info(archive, d, b)
+        print()
 
-            except requests.exceptions.RequestException as e:
-                print("Request getting mp3 failed.")
 
-            except Exception as e:
-                print("Error downloading mp3.")
-                print(e)
-
-def get_directory_name(name, datetime):
-    prefix = ""
-    if len(name) > 0:
-        prefix = name + "_"
+def screen_download_all(archive, search_key, prefix, deep_search=False):
+    """Automatically download all broadcasts matching a search pattern.
 
-    return prefix + datetime.strftime("%d-%m-%Y")
+    Args:
+        archive: SuperArchive instance
+        search_key: Search pattern
+        prefix: Directory prefix for downloads
+        deep_search: If True, search in description as well (slower)
+    """
+    results = archive.get_broadcasts_by_regex(
+        search_key, deep_search=deep_search)
+    if not results:
+        print(f"No broadcasts found matching: {search_key}")
+        return
 
-def make_directory(name, datetime):
-    """Creates the download subdirectory for the given name and datetime."""
-    dirname = get_directory_name(name, datetime)
-    if not os.path.exists(dirname):
-        os.makedirs(dirname)
+    print(f"Found {len(results)} broadcast(s) to download.\n")
+    print("Starting downloads...\n")
 
-def write_html_file(name, datetime, description):
-    """Stores broadcast description into a html file."""
+    success_count = 0
+    for d, b in results:
+        date, title = archive.get_broadcast(d, b)
+        print(f"{date.strftime('%a %d.%m.%Y %H:%M:%S')} - {title}")
 
-    longname = get_directory_name(name, datetime)
-    filepath = os.path.join(longname, longname + ".html")
-    file = open(filepath, 'w+')
-    file.write("<!DOCTYPE html>\n")
-    file.write("<html>\n")
-    file.write("<head>\n")
-    file.write("<title>\n")
-    file.write("%s %s\n" % (name, datetime.strftime("%d.%m.%Y")))
-    file.write("</title>\n")
-    file.write("<meta charset = \"utf-8\">\n")
-    file.write("</head>\n")
-    file.write("<body>\n")
-    file.write("%s %s" % (name, datetime.strftime("%d.%m.%Y")))
-    file.write(description)
-    file.write("</body>\n")
-    file.write("</html>")
-    file.close()
-
-def write_mp3_file(name, datetime, url):
-    import requests
-
-    longname = get_directory_name(name, datetime)
-    filepath = os.path.join(longname, longname + ".mp3")
+        if archive.download_broadcast(d, b, prefix):
+            success_count += 1
+        print()
 
-    print("Fetching mp3...")
-    r = requests.get(url, stream=True)
-    if r.status_code == 200:
-        with open(filepath, 'wb') as f:
-            f.write(r.content)
-    else:
-        print("Error downloading mp3. Status code: %d" % r.status_code)
+    print(f"\nDownload completed: {success_count}/{len(results)} successful")
 
-def screen_search(key):
-    a = Archive()
-    for d, b in a.get_broadcasts_by_regex(key):
-        print_broadcast_info(a, d, b)
-        print()
 
 def print_broadcast_info(archive, day, broadcast):
-    a, d, b = archive, day, broadcast
-    date, title = a.get_broadcast(d, b)
+    """Print detailed information about a broadcast."""
+    date, title = archive.get_broadcast(day, broadcast)
 
     print("%s   %s" % (date.strftime("%a %d.%m.%Y  %H:%M:%S"), title))
-    print("  %s" % a.get_broadcast_subtitle(d, b))
-    print("  Broadcast: %s" % a.get_broadcast_url(d, b))
-    print("  Player: %s" % a.get_player_url(d, b))
-    print("  Program key: %s" % a.get_broadcast_pk(d, b))
+    print("  %s" % archive.get_broadcast_subtitle(day, broadcast))
+    url = archive.get_broadcast_url(day, broadcast)
+    print("  Stream: %s" % (url if url else "Not available"))
+    print("  Player: %s" % archive.get_player_url(day, broadcast))
+    print("  Program key: %s" % archive.get_broadcast_pk(day, broadcast))
+
 
 if __name__ == "__main__":
 
     try:
-        opts, args = getopt.getopt(sys.argv[1:], "hcs:",
-                ["help", "choose", "search="])
+        opts, args = getopt.getopt(sys.argv[1:], "hcs:p:d:e", [
+                                   "help", "choose", "search=", "prefix=", "download=", "extended-search"])
     except getopt.GetoptError as err:
         print(err)
         screen_help()
         sys.exit(2)
 
+    archive = None
+    search_key = None
+    download_key = None
+    prefix = ""
+    choose_mode = False
+    extended_search = False
+
     for o, a in opts:
         if o in ["-h", "--help"]:
             screen_help()
+            sys.exit(0)
         if o in ["-c", "--choose"]:
-            screen_choose()
+            choose_mode = True
         if o in ["-s", "--search"]:
-            screen_search(a)
+            search_key = a
+        if o in ["-d", "--download"]:
+            download_key = a
+        if o in ["-p", "--prefix"]:
+            prefix = a
+        if o in ["-e", "--extended-search"]:
+            extended_search = True
+
+    # Initialize archive
+    archive = SuperArchive(days=30)
+
+    # Execute requested action
+    if choose_mode:
+        screen_choose(archive)
+    elif download_key:
+        if not prefix:
+            print("Error: --prefix required when using --download")
+            print("Example: oe1archive.py -d 'Brunch' -p 'Brunch'")
+            sys.exit(1)
+        screen_download_all(
+            archive,
+            download_key,
+            prefix,
+            deep_search=extended_search)
+    elif search_key:
+        screen_search(archive, search_key, deep_search=extended_search)
+    else:
+        screen_help()