Skip to content

Repository files navigation

iTunes Podcast Search Package 🎧

A comprehensive Swift package for searching and discovering podcasts using the iTunes API. This package provides a simple yet powerful interface for podcast discovery, trending lists, and advanced filtering capabilities.

Features

  • 🔍 Comprehensive Search: Search podcasts by term, genre, country, language, and more
  • 📈 Trending Podcasts: Get trending podcasts by country and category
  • 🎯 Advanced Filtering: Filter by explicit content, specific attributes, and custom criteria
  • 🚀 Batch Operations: Perform multiple searches and lookups concurrently
  • 💾 Built-in Caching: Automatic response caching for improved performance
  • 🌍 Global Support: Support for all iTunes Store countries and languages
  • 📱 SwiftUI Ready: Codable models perfect for SwiftUI integration
  • Async/Await: Modern Swift concurrency support

Installation

Swift Package Manager

Add the package to your Package.swift:

dependencies: [
    .package(url: "https://github.com/yourusername/itunes-podcast-search", from: "1.0.0")
]

Or add it through Xcode:

  1. File → Add Package Dependencies
  2. Enter the repository URL
  3. Click Add Package

Requirements

  • iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+
  • Swift 5.5+
  • SwiftyJSON

Quick Start

import ItunesPodcastSearch

// Simple search
let results = try await searchPodcasts(term: "technology")
if let podcasts = results.podcastList {
    for podcast in podcasts {
        print("\(podcast.title ?? "Unknown") by \(podcast.author ?? "Unknown")")
    }
}

// Get trending podcasts
let trending = try await getTrendingPodcastItems(country: .unitedStates, limit: 20)

// Look up specific podcasts
let podcast = try await lookupPodcast(id: "1234567890")

Core Functions

Basic Search

searchPodcasts()

Search for podcasts with various filters.

func searchPodcasts(
    term: String? = nil,
    country: Country? = nil,
    entity: Entity? = .podcastAndEpisode,
    attribute: String? = nil,
    genreId: PodcastGenre? = nil,
    lang: Language? = nil,
    version: Int? = 2,
    explicit: String? = nil,
    limit: Int? = nil
) async throws -> PodcastResult

Example:

let results = try await searchPodcasts(
    term: "Swift programming",
    country: .unitedStates,
    genreId: .technology,
    limit: 50
)

Advanced Search

searchPodcastsAdvanced()

Perform advanced searches with comprehensive filtering and sorting.

let filters = PodcastSearchFilters(
    searchTerm: "iOS development",
    country: .unitedStates,
    genre: .technology,
    language: .english,
    explicitContent: .no
)

let results = try await searchPodcastsAdvanced(
    filters: filters,
    sortBy: .dateNewest,
    limit: 25
)

searchPodcastsByAttribute()

Search within specific podcast attributes.

// Search only in podcast titles
let titleResults = try await searchPodcastsByAttribute(
    term: "AI",
    attribute: .titleTerm,
    country: .unitedStates
)

// Search only in author names
let authorResults = try await searchPodcastsByAttribute(
    term: "John Doe",
    attribute: .authorTerm
)

Trending & Discovery

getTrendingPodcastItems()

Get trending podcasts for a specific country.

let trending = try await getTrendingPodcastItems(
    country: .unitedStates,
    limit: 50
)

getTrendingPodcastsByCategory()

Get trending podcasts within a specific genre.

let techTrending = try await getTrendingPodcastsByCategory(
    category: .technology,
    country: .unitedStates,
    limit: 30
)

getRecommendedPodcasts()

Get podcast recommendations based on a seed podcast.

if let seedPodcast = myFavoritePodcast {
    let recommendations = try await getRecommendedPodcasts(
        basedOn: seedPodcast,
        limit: 20
    )
}

Lookup Operations

lookupPodcast() / lookupPodcasts()

Look up podcasts by their iTunes IDs.

// Single podcast lookup
let podcast = try await lookupPodcast(id: "1234567890")

// Multiple podcasts lookup
let podcasts = try await lookupPodcasts(ids: ["123", "456", "789"])

Batch Operations

batchSearchPodcasts()

Search for multiple terms concurrently.

let searchTerms = ["AI", "Machine Learning", "Data Science"]
let results = try await batchSearchPodcasts(searchTerms, limit: 20)

for (term, result) in results {
    print("Results for '\(term)': \(result.totalCount ?? 0) podcasts")
}

batchLookupPodcasts()

Look up multiple podcast IDs efficiently.

let ids = ["123", "456", "789", "101112"]
let results = try await batchLookupPodcasts(ids)

for (id, podcast) in results {
    if let podcast = podcast {
        print("Found: \(podcast.title ?? "Unknown")")
    } else {
        print("Podcast \(id) not found")
    }
}

Data Models

PodcastResult

Container for search results with additional utility methods.

public class PodcastResult {
    public let totalCount: Int?
    public let podcastList: [Podcast]?
    
    // Filter explicit content
    func filterExplicit(_ allowExplicit: Bool) -> PodcastResult
    
    // Group by genre
    func groupedByGenre() -> [String: [Podcast]]
}

Podcast

Comprehensive podcast model with all available metadata.

public class Podcast {
    public let id: String
    public let title: String?
    public let author: String?
    public let description: String?
    public let image: URL?
    public let feedURL: URL?
    public let genres: [String]?
    public let primaryGenre: String?
    public let country: String?
    public let isExplicit: Bool?
    public let trackCount: Int?
    public let publicationDate: Date?
    
    // Utility properties
    public var bestArtworkURL: URL?
    public var genresString: String
    
    // Search matching
    func matches(searchTerm: String) -> Bool
}

Enums & Types

PodcastGenre

All available podcast genres with display names.

public enum PodcastGenre: String, CaseIterable {
    case arts = "1301"
    case business = "1321"
    case comedy = "1303"
    case education = "1304"
    case technology = "1318"
    // ... and more
    
    public var displayName: String { /* Human-readable name */ }
    public static func fromString(_ string: String) -> PodcastGenre?
}

Country

Comprehensive country support with localized names.

public enum Country: String, CaseIterable {
    case unitedStates = "US"
    case unitedKingdom = "GB"
    case canada = "CA"
    // ... all countries supported
    
    public var displayName: String { /* Localized country name */ }
    public static func fromCountryCode(_ code: String) -> Country?
}

PodcastSearchFilters

Structured search filters for advanced searching.

public struct PodcastSearchFilters {
    public let searchTerm: String?
    public let country: Country?
    public let entity: Entity?
    public let genre: PodcastGenre?
    public let language: Language?
    public let explicitContent: ExplicitContentFilter?
}

PodcastSortOption

Options for sorting search results.

public enum PodcastSortOption {
    case relevance      // Default iTunes relevance
    case title          // Alphabetical by title
    case author         // Alphabetical by author
    case dateNewest     // Newest first
    case dateOldest     // Oldest first
}

Configuration & Caching

Manager Configuration

Customize request behavior and timeouts.

// Configure the manager
ItunesManager.shared.configuration = ItunesManager.Configuration(
    timeout: 30.0,
    cachePolicy: .useProtocolCachePolicy,
    retryCount: 3
)

Built-in Caching

Automatic response caching for improved performance.

// Search with automatic caching (5-minute TTL)
let results = try await ItunesManager.searchWithCache(
    term: "programming",
    country: .unitedStates,
    limit: 50
)

// Clear cache manually
PodcastCache.shared.clearCache()

Error Handling

The package provides comprehensive error handling with custom error types.

do {
    let results = try await searchPodcasts(term: "nonexistent")
} catch ItunesManagerError.invalidURL {
    print("Invalid URL provided")
} catch ItunesManagerError.httpError(let statusCode) {
    print("HTTP error: \(statusCode)")
} catch ItunesManagerError.networkError(let error) {
    print("Network error: \(error.localizedDescription)")
} catch {
    print("Other error: \(error)")
}

Advanced Usage Examples

Building a Podcast Discovery App

class PodcastDiscoveryViewModel: ObservableObject {
    @Published var searchResults: [Podcast] = []
    @Published var trendingPodcasts: [Podcast] = []
    @Published var isLoading = false
    
    func searchPodcasts(term: String) async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            let results = try await searchPodcastsAdvanced(
                filters: PodcastSearchFilters(
                    searchTerm: term,
                    country: .unitedStates,
                    explicitContent: .no
                ),
                sortBy: .relevance,
                limit: 50
            )
            
            await MainActor.run {
                self.searchResults = results.podcastList ?? []
            }
        } catch {
            print("Search failed: \(error)")
        }
    }
    
    func loadTrending() async {
        do {
            let trending = try await getTrendingPodcastItems(
                country: .unitedStates,
                limit: 20
            )
            
            await MainActor.run {
                self.trendingPodcasts = trending.podcastList ?? []
            }
        } catch {
            print("Failed to load trending: \(error)")
        }
    }
}

Category-based Browsing

class CategoryBrowserViewModel: ObservableObject {
    @Published var podcastsByCategory: [String: [Podcast]] = [:]
    
    func loadAllCategories() async {
        let categories: [PodcastGenre] = [.technology, .business, .education, .comedy]
        
        await withTaskGroup(of: (PodcastGenre, [Podcast]).self) { group in
            for category in categories {
                group.addTask {
                    do {
                        let result = try await getTrendingPodcastsByCategory(
                            category: category,
                            country: .unitedStates,
                            limit: 10
                        )
                        return (category, result.podcastList ?? [])
                    } catch {
                        return (category, [])
                    }
                }
            }
            
            for await (category, podcasts) in group {
                await MainActor.run {
                    self.podcastsByCategory[category.displayName] = podcasts
                }
            }
        }
    }
}

Personalized Recommendations

class RecommendationEngine {
    func getPersonalizedRecommendations(
        favoriteGenres: [PodcastGenre],
        favoritePodcasts: [Podcast],
        country: Country = .unitedStates
    ) async throws -> [Podcast] {
        var recommendations: [Podcast] = []
        
        // Get recommendations based on favorite podcasts
        for podcast in favoritePodcasts.prefix(3) {
            let similar = try await getRecommendedPodcasts(
                basedOn: podcast,
                limit: 5
            )
            recommendations.append(contentsOf: similar.podcastList ?? [])
        }
        
        // Add trending from favorite genres
        for genre in favoriteGenres {
            let trending = try await getTrendingPodcastsByCategory(
                category: genre,
                country: country,
                limit: 3
            )
            recommendations.append(contentsOf: trending.podcastList ?? [])
        }
        
        // Remove duplicates and return
        return Array(Set(recommendations.map(\.id)))
            .compactMap { id in recommendations.first(where: { $0.id == id }) }
            .prefix(20)
            .shuffled()
    }
}

Performance Tips

  1. Use Batch Operations: When looking up multiple podcasts, use batchLookupPodcasts() instead of individual calls.

  2. Enable Caching: Use searchWithCache() for frequently accessed searches.

  3. Limit Results: Always specify appropriate limits to avoid unnecessary data transfer.

  4. Concurrent Searches: Use Task groups for concurrent operations when possible.

  5. Error Handling: Always implement proper error handling for network operations.

Migration Guide

This package is designed to be fully backward compatible. If you're upgrading from an earlier version:

  • All existing function signatures remain unchanged
  • New parameters are optional with sensible defaults
  • Enhanced models include all original properties
  • No breaking changes to public APIs

Contributing

We welcome contributions! Please feel free to submit pull requests, report bugs, or suggest new features.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

For questions, issues, or feature requests, please open an issue on GitHub or contact the maintainers.


Happy podcast discovering! 🎧

About

iTunes Podcast Api Manager

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages