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.
- 🔍 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
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:
- File → Add Package Dependencies
- Enter the repository URL
- Click Add Package
- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+
- Swift 5.5+
- SwiftyJSON
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")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 -> PodcastResultExample:
let results = try await searchPodcasts(
term: "Swift programming",
country: .unitedStates,
genreId: .technology,
limit: 50
)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
)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
)Get trending podcasts for a specific country.
let trending = try await getTrendingPodcastItems(
country: .unitedStates,
limit: 50
)Get trending podcasts within a specific genre.
let techTrending = try await getTrendingPodcastsByCategory(
category: .technology,
country: .unitedStates,
limit: 30
)Get podcast recommendations based on a seed podcast.
if let seedPodcast = myFavoritePodcast {
let recommendations = try await getRecommendedPodcasts(
basedOn: seedPodcast,
limit: 20
)
}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"])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")
}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")
}
}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]]
}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
}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?
}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?
}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?
}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
}Customize request behavior and timeouts.
// Configure the manager
ItunesManager.shared.configuration = ItunesManager.Configuration(
timeout: 30.0,
cachePolicy: .useProtocolCachePolicy,
retryCount: 3
)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()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)")
}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)")
}
}
}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
}
}
}
}
}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()
}
}-
Use Batch Operations: When looking up multiple podcasts, use
batchLookupPodcasts()instead of individual calls. -
Enable Caching: Use
searchWithCache()for frequently accessed searches. -
Limit Results: Always specify appropriate limits to avoid unnecessary data transfer.
-
Concurrent Searches: Use Task groups for concurrent operations when possible.
-
Error Handling: Always implement proper error handling for network operations.
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
We welcome contributions! Please feel free to submit pull requests, report bugs, or suggest new features.
This project is licensed under the MIT License - see the LICENSE file for details.
For questions, issues, or feature requests, please open an issue on GitHub or contact the maintainers.
Happy podcast discovering! 🎧