├── tests ├── __init__.py ├── test_random_episodes.py ├── test_search.py ├── test_podcast_lookup.py ├── test_trending_podcasts.py ├── test_recent_episodes.py └── test_episode_lookup.py ├── podcastindex ├── __init__.py └── podcastindex.py ├── requirements.txt ├── .coveragerc ├── .travis.yml ├── pyproject.toml ├── LICENSE ├── .github └── workflows │ └── publish-to-pypi.yml ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /podcastindex/__init__.py: -------------------------------------------------------------------------------- 1 | from .podcastindex import init, get_config_from_env 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | 3 | # For development 4 | black;python_version>='3.6' 5 | flake8;python_version>='3.6' 6 | ipython 7 | setuptools 8 | twine 9 | wheel 10 | 11 | # For test 12 | codecov 13 | coverage 14 | pytest 15 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | # omit anything in a .venv directory anywhere 4 | */.venv/* 5 | # omit anything in a test or tests directory anywhere 6 | */test/* 7 | */tests/* 8 | source = 9 | podcastindex 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | - "3.8" 9 | # command to install dependencies 10 | install: 11 | - pip install -r requirements.txt 12 | # command to run tests 13 | script: 14 | - coverage run -m pytest --log-cli-level=INFO 15 | after_success: 16 | - codecov 17 | -------------------------------------------------------------------------------- /tests/test_random_episodes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import podcastindex 4 | 5 | logging.basicConfig(level=logging.DEBUG) 6 | logger = logging.getLogger() 7 | 8 | def test_random_episodes(): 9 | config = podcastindex.get_config_from_env() 10 | index = podcastindex.init(config) 11 | 12 | # Basic test 13 | results = index.randomEpisodes(max=1) 14 | assert len(results["episodes"]) == 1, "Expected exactly one episode." 15 | 16 | # Test that changing the `max` parameter impacts the number of episodes returned 17 | results = index.randomEpisodes(max=3) 18 | assert len(results["episodes"]) == 3, "Expected exactly three episodes." 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "python-podcastindex" 7 | version = "1.15.0" 8 | authors = [ 9 | { name="Sarvagya (Survy) Vaish", email="sarvagya.vaish7@gmail.com" }, 10 | ] 11 | description = "A python wrapper for the Podcast Index API (podcastindex.org)." 12 | readme = "README.md" 13 | requires-python = ">=2.7" 14 | license = "MIT" 15 | classifiers = [ 16 | "Programming Language :: Python :: 2.7", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.4", 19 | "Programming Language :: Python :: 3.5", 20 | "Programming Language :: Python :: 3.6", 21 | "Programming Language :: Python :: 3.7", 22 | ] 23 | dependencies = [ 24 | "requests", 25 | ] 26 | 27 | [project.urls] 28 | "Homepage" = "https://github.com/SarvagyaVaish/python-podcastindex" 29 | "Bug Tracker" = "https://github.com/SarvagyaVaish/python-podcastindex/issues" 30 | 31 | [project.scripts] 32 | realpython = "podcastindex.__main__:main" 33 | -------------------------------------------------------------------------------- /tests/test_search.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import podcastindex 4 | 5 | logging.basicConfig(level=logging.DEBUG) 6 | logger = logging.getLogger() 7 | 8 | 9 | def test_search_found(): 10 | config = podcastindex.get_config_from_env() 11 | index = podcastindex.init(config) 12 | 13 | query_str = "This American Life" 14 | results = index.search(query_str) 15 | found = False 16 | 17 | for feed in results["feeds"]: 18 | title = feed["title"] 19 | if query_str == title: 20 | # Found a matching feed 21 | found = True 22 | break 23 | 24 | assert found, "Count not find podcast that should be in the feed: {}".format(query_str) 25 | 26 | 27 | def test_search_clean(): 28 | config = podcastindex.get_config_from_env() 29 | index = podcastindex.init(config) 30 | 31 | query_str = "Sex" 32 | results_dirty = index.search(query_str, clean=False) 33 | results_clean = index.search(query_str, clean=True) 34 | 35 | assert len(results_clean["feeds"]) < len(results_dirty["feeds"]) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Survy Vaish 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to PyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-and-publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 13 | 14 | steps: 15 | - name: Checkout repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up python 3.10 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.10" 22 | cache: 'pip' 23 | 24 | - name: Install build dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install build 28 | 29 | - name: Build package 30 | run: python -m build 31 | 32 | # Uncomment to test with PyPI test server 33 | # 34 | # - name: Publish to Test PyPI 35 | # uses: pypa/gh-action-pypi-publish@release/v1 36 | # with: 37 | # repository-url: https://test.pypi.org/legacy/ 38 | # password: ${{ secrets.TEST_PYPI_API_TOKEN }} 39 | 40 | - name: Publish to PyPI 41 | uses: pypa/gh-action-pypi-publish@release/v1 42 | with: 43 | password: ${{ secrets.PYPI_API_TOKEN }} 44 | -------------------------------------------------------------------------------- /tests/test_podcast_lookup.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import podcastindex 4 | 5 | logging.basicConfig(level=logging.DEBUG) 6 | logger = logging.getLogger() 7 | 8 | podcastTitle = "This American Life" 9 | feedUrl = "http://feed.thisamericanlife.org/talpodcast" 10 | feedId = 522613 11 | itunesId = 201671138 12 | guid = "f1ebeaa1-bc5a-534f-8528-0738ae374d55" 13 | 14 | 15 | def test_podcast_lookup_by_feedurl(): 16 | config = podcastindex.get_config_from_env() 17 | index = podcastindex.init(config) 18 | 19 | results = index.podcastByFeedUrl(feedUrl) 20 | assert results["feed"]["title"] == podcastTitle, "Did not find the right podcast when doing lookup by URL" 21 | 22 | 23 | def test_podcast_lookup_by_byfeedid(): 24 | config = podcastindex.get_config_from_env() 25 | index = podcastindex.init(config) 26 | 27 | results = index.podcastByFeedId(feedId) 28 | assert results["feed"]["title"] == podcastTitle, "Did not find the right podcast when doing lookup by Feed ID" 29 | 30 | 31 | def test_podcast_lookup_by_byitunesid(): 32 | config = podcastindex.get_config_from_env() 33 | index = podcastindex.init(config) 34 | 35 | results = index.podcastByItunesId(itunesId) 36 | assert results["feed"]["title"] == podcastTitle, "Did not find the right podcast when doing lookup by Itunes ID" 37 | 38 | def test_podcast_lookup_by_byguid(): 39 | config = podcastindex.get_config_from_env() 40 | index = podcastindex.init(config) 41 | 42 | results = index.podcastByGuid(guid) 43 | assert results["feed"]["title"] == podcastTitle, "Did not find the right podcast when doing lookup by guid" 44 | -------------------------------------------------------------------------------- /tests/test_trending_podcasts.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | import podcastindex 5 | 6 | logging.basicConfig(level=logging.DEBUG) 7 | logger = logging.getLogger() 8 | 9 | 10 | def _log_results(context, results): 11 | logger.ingo("Podcast results from {}".format(context)) 12 | for feed in results["feeds"]: 13 | logger.info( 14 | "Podcast Id: {} \t Title: {} \t Newest item publish time: {} \t Categories: {}".format( 15 | feed["id"], feed["title"], feed["newestItemPublishTime"], feed["categories"]) 16 | ) 17 | 18 | 19 | def test_trending_podcasts_max(): 20 | config = podcastindex.get_config_from_env() 21 | index = podcastindex.init(config) 22 | 23 | results = index.trendingPodcasts(max=15) 24 | assert len(results["feeds"] 25 | ) == 15, "By default we expect to get back 10 results" 26 | 27 | 28 | def test_trending_podcasts_since(): 29 | config = podcastindex.get_config_from_env() 30 | index = podcastindex.init(config) 31 | 32 | results = index.trendingPodcasts(since=-(24 * 3600)) 33 | for feed in results["feeds"]: 34 | last_updated = feed["newestItemPublishTime"] 35 | assert time.time() - last_updated < 24 * 3600 36 | 37 | 38 | def test_trending_podcasts_lang(): 39 | config = podcastindex.get_config_from_env() 40 | index = podcastindex.init(config) 41 | 42 | results = index.trendingPodcasts(lang=["es"]) 43 | for feed in results["feeds"]: 44 | assert feed["language"] == "es" 45 | 46 | 47 | def test_trending_podcasts_categories(): 48 | config = podcastindex.get_config_from_env() 49 | index = podcastindex.init(config) 50 | categories = ["News", "Comedy"] 51 | 52 | results = index.trendingPodcasts(categories=categories) 53 | for feed in results["feeds"]: 54 | assert categories[0] in feed["categories"].values( 55 | ) or categories[1] in feed["categories"].values() 56 | 57 | 58 | def test_trending_podcasts_not_categories(): 59 | config = podcastindex.get_config_from_env() 60 | index = podcastindex.init(config) 61 | categories = ["News", "Comedy"] 62 | 63 | results = index.trendingPodcasts(not_categories=categories) 64 | for feed in results["feeds"]: 65 | for id in feed["categories"]: 66 | assert not feed["categories"][id] in categories 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # IDEs 132 | .vscode/ 133 | -------------------------------------------------------------------------------- /tests/test_recent_episodes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | import podcastindex 5 | 6 | logging.basicConfig(level=logging.DEBUG) 7 | logger = logging.getLogger() 8 | 9 | feedUrl = "http://feed.thisamericanlife.org/talpodcast" 10 | feedId = 522613 11 | itunesId = 201671138 12 | 13 | 14 | def _log_results(context, results): 15 | logger.info("Episode results from {}".format(context)) 16 | for episode in results["items"]: 17 | logger.info( 18 | "Episode Id: {} \t Date: {}, {} \t ".format( 19 | episode["id"], episode["datePublished"], episode["datePublishedPretty"] 20 | ) 21 | ) 22 | 23 | 24 | # # Uncomment once the API is fixed. 25 | # def test_recent_episodes(): 26 | # config = podcastindex.get_config_from_env() 27 | # index = podcastindex.init(config) 28 | 29 | # results = index.recentEpisodes(max=10) 30 | # _log_results("test_recent_episodes", results) 31 | 32 | # # Last episode should be within the last day 33 | # last_episode_timestamp = results["items"][0]["datePublished"] 34 | # assert time.time() - last_episode_timestamp < 24 * 3600 35 | 36 | # # Episodes are ordered in reverse chronological order 37 | # for i in range(1, len(results["items"])): 38 | # more_recent_timestamp = results["items"][i - 1]["datePublished"] 39 | # next_item_timestamp = results["items"][i]["datePublished"] 40 | # assert ( 41 | # more_recent_timestamp >= next_item_timestamp 42 | # ), "Recent episodes should be returned in reverse chronological order" 43 | 44 | 45 | def test_recent_episodes_max(): 46 | config = podcastindex.get_config_from_env() 47 | index = podcastindex.init(config) 48 | 49 | results = index.recentEpisodes(max=15) 50 | assert len(results["items"]) == 15, "By default we expect to get back 10 results" 51 | 52 | 53 | # # Uncomment once the API is fixed. 54 | # def test_recent_episodes_before_id(): 55 | # config = podcastindex.get_config_from_env() 56 | # index = podcastindex.init(config) 57 | 58 | # results = index.recentEpisodes(max=10) 59 | # _log_results("test_recent_episodes_before_id", results) 60 | # prev_oldest_id = results["items"][-1]["id"] 61 | # prev_oldest_timestamp = results["items"][-1]["datePublished"] 62 | 63 | # results = index.recentEpisodes(max=1, before_episode_id=prev_oldest_id) 64 | # _log_results("test_recent_episodes_before_id", results) 65 | # for episode in results["items"]: 66 | # episode_timestamp = episode["datePublished"] 67 | # assert episode_timestamp >= prev_oldest_timestamp, "Using before episode id, we should get older results" 68 | -------------------------------------------------------------------------------- /tests/test_episode_lookup.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | import requests 5 | 6 | import podcastindex 7 | 8 | logging.basicConfig(level=logging.DEBUG) 9 | logger = logging.getLogger() 10 | 11 | feedUrl = "https://lexfridman.com/feed/podcast/" 12 | feedId = 745287 13 | itunesId = 1434243584 14 | badItunesId = "abcdefg1234" 15 | podcastGuid = "7eeae9d1-141e-5133-9e8f-6c1da695e40c" 16 | 17 | 18 | def test_episode_lookup_by_feedid(): 19 | config = podcastindex.get_config_from_env() 20 | index = podcastindex.init(config) 21 | 22 | # Test basic episode retrieval 23 | results = index.episodesByFeedId(feedId) 24 | assert len(results["items"]) > 0, "No episodes found when looking up episodes by feed ID." 25 | assert ( 26 | results["items"][0]["feedItunesId"] == itunesId 27 | ), "Episodes found do not belong to the feed ID used in the query" 28 | 29 | # Test with since 30 | most_recent_ep_timestamp = results["items"][0]["datePublished"] 31 | since_timestamp = most_recent_ep_timestamp - 1 32 | 33 | results = index.episodesByFeedId(feedId, since=since_timestamp) 34 | assert ( 35 | len(results["items"]) > 0 36 | ), "We should get back at least one episode when looking for episodes since right before the most recent episode." 37 | 38 | for episode in results["items"]: 39 | assert ( 40 | episode["datePublished"] >= since_timestamp 41 | ), "Looking for episodes since right before the most recent episode is not correct." 42 | 43 | 44 | def test_episode_lookup_by_feedurl(): 45 | config = podcastindex.get_config_from_env() 46 | index = podcastindex.init(config) 47 | 48 | results = index.episodesByFeedUrl(feedUrl) 49 | assert len(results["items"]) > 0, "No episodes found when looking up episodes by feed URL." 50 | assert ( 51 | results["items"][0]["feedItunesId"] == itunesId 52 | ), "Episodes found do not belong to the feed URL used in the query" 53 | 54 | 55 | def test_episode_lookup_by_itunesid(): 56 | config = podcastindex.get_config_from_env() 57 | index = podcastindex.init(config) 58 | 59 | results = index.episodesByItunesId(itunesId) 60 | assert len(results["items"]) > 0, "No episodes found when looking up episodes by Itunes ID." 61 | assert ( 62 | results["items"][0]["feedItunesId"] == itunesId 63 | ), "Episodes found do not belong to the Itunes ID used in the query" 64 | 65 | 66 | def test_episode_lookup_by_podcastguid(): 67 | config = podcastindex.get_config_from_env() 68 | index = podcastindex.init(config) 69 | 70 | results = index.episodesByPodcastGuid(podcastGuid) 71 | assert len(results["items"]) > 0, "No episodes found when looking up episodes by podcast GUID." 72 | assert ( 73 | results["items"][0]["feedItunesId"] == itunesId 74 | ), "Episodes found do not belong to the podcast GUID used in the query" 75 | 76 | 77 | def test_episode_lookup_by_id(): 78 | config = podcastindex.get_config_from_env() 79 | index = podcastindex.init(config) 80 | 81 | results = index.episodesByItunesId(itunesId) 82 | latest_episode_id = results["items"][0]["id"] 83 | 84 | results = index.episodeById(latest_episode_id) 85 | assert ( 86 | results["episode"]["id"] == latest_episode_id 87 | ), "Episode fetched by ID should match ID used in query" 88 | 89 | 90 | def test_episode_lookup_by_guid(): 91 | config = podcastindex.get_config_from_env() 92 | index = podcastindex.init(config) 93 | 94 | results = index.episodesByFeedUrl(feedUrl) 95 | latest_episode_guid = results["items"][0]["guid"] 96 | 97 | results = index.episodeByGuid(latest_episode_guid, feedUrl) 98 | assert ( 99 | results["episode"]["guid"] == latest_episode_guid 100 | ), "Episode fetched by GUID and FEEDURL should match GUID used in query" 101 | 102 | results = index.episodeByGuid( 103 | latest_episode_guid, feedid=results["episode"]["feedId"] 104 | ) 105 | assert ( 106 | results["episode"]["guid"] == latest_episode_guid 107 | ), "Episode fetched by GUID and FEEDID should match GUID used in query" 108 | 109 | results = index.episodeByGuid( 110 | latest_episode_guid, podcastguid=results["episode"]["podcastGuid"] 111 | ) 112 | assert ( 113 | results["episode"]["guid"] == latest_episode_guid 114 | ), "Episode fetched by GUID and PODCASTGUID should match GUID used in query" 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-podcastindex 2 | 3 | A python wrapper for the Podcast Index API (podcastindex.org) 4 | 5 | ## Installation 6 | 7 | ``` 8 | pip install python-podcastindex 9 | ``` 10 | 11 | ## Usage 12 | 13 | 1. [ Init the podcast index ](#init) 14 | 1. [ Search ](#search) 15 | 1. [ Podcasts ](#podcasts) 16 | 1. [ Episodes of a podcast ](#episode_of_a_podcast) 17 | 1. [ Episode by ID ](#episodes_by_id) 18 | 1. [ Recent episodes ](#recent_episodes) 19 | 20 | 21 | 22 | ### Init the podcast index 23 | ```python 24 | import podcastindex 25 | 26 | config = { 27 | "api_key": "YOUR API KEY HERE", 28 | "api_secret": "YOUR API SECRET HERE" 29 | } 30 | 31 | index = podcastindex.init(config) 32 | ``` 33 | 34 | 35 | ### Search 36 | 37 | ```python 38 | result = index.search("This American Life") 39 | result = index.search("This American Life", clean=True) 40 | ``` 41 | 42 |
43 | Click to see sample result! 44 | 45 | ```javascript 46 | { 47 | "status": "true", 48 | "feeds": [ 49 | { 50 | "id": 522613, 51 | "title": "This American Life", 52 | "url": "http://feed.thisamericanlife.org/talpodcast", 53 | "originalUrl": "http://feed.thisamericanlife.org/talpodcast", 54 | "link": "https://www.thisamericanlife.org", 55 | "description": "This American Life is a weekly public ...", 56 | "author": "This American Life", 57 | "ownerName": "", 58 | "image": "https://files.thisamericanlife.org/sites/all/themes/thislife/img/tal-name-1400x1400.png", 59 | "artwork": "https://files.thisamericanlife.org/sites/all/themes/thislife/img/tal-name-1400x1400.png", 60 | "lastUpdateTime": 1607323495, 61 | "lastCrawlTime": 1607632436, 62 | "lastParseTime": 1607323495, 63 | "lastGoodHttpStatusTime": 1607632436, 64 | "lastHttpStatus": 200, 65 | "contentType": "text/xml; charset=UTF-8", 66 | "itunesId": 201671138, 67 | "generator": null, 68 | "language": "en", 69 | "type": 0, 70 | "dead": 0, 71 | "crawlErrors": 0, 72 | "parseErrors": 0, 73 | "categories": { 74 | "77": "Society", 75 | "78": "Culture", 76 | "1": "Arts", 77 | "55": "News", 78 | "59": "Politics" 79 | }, 80 | "locked": 0, 81 | "imageUrlHash": 1124696616 82 | }, 83 | ... 84 | ], 85 | "count": 8, 86 | "query": "This American Life", 87 | "description": "Found matching feeds." 88 | } 89 | ``` 90 |
91 | 92 | 93 | ### Podcasts 94 | 95 | ```python 96 | results = index.podcastByFeedId(522613) 97 | results = index.podcastByFeedUrl("http://feed.thisamericanlife.org/talpodcast") 98 | results = index.podcastByItunesId(201671138) 99 | ``` 100 | 101 |
102 | Click to see sample result! 103 | 104 | ```javascript 105 | { 106 | "status": "true", 107 | "query": { 108 | "id": "201671138" 109 | }, 110 | "feed": { 111 | "id": 522613, 112 | "title": "This American Life", 113 | "url": "http://feed.thisamericanlife.org/talpodcast", 114 | "originalUrl": "http://feed.thisamericanlife.org/talpodcast", 115 | "link": "https://www.thisamericanlife.org", 116 | "description": "This American Life is a weekly public radio show, heard by 2.2 million people on more than 500 stations. Another 2.5 million people download the weekly podcast. It is hosted by Ira Glass, produced in collaboration with Chicago Public Media, delivered to stations by PRX The Public Radio Exchange, and has won all of the major broadcasting awards.", 117 | "author": "This American Life", 118 | "ownerName": "", 119 | "image": "https://files.thisamericanlife.org/sites/all/themes/thislife/img/tal-name-1400x1400.png", 120 | "artwork": "https://files.thisamericanlife.org/sites/all/themes/thislife/img/tal-name-1400x1400.png", 121 | "lastUpdateTime": 1607927945, 122 | "lastCrawlTime": 1608430718, 123 | "lastParseTime": 1608376393, 124 | "lastGoodHttpStatusTime": 1608430718, 125 | "lastHttpStatus": 200, 126 | "contentType": "text/xml; charset=UTF-8", 127 | "itunesId": 201671138, 128 | "generator": null, 129 | "language": "en", 130 | "type": 0, 131 | "dead": 0, 132 | "crawlErrors": 0, 133 | "parseErrors": 0, 134 | "locked": 0 135 | }, 136 | "description": "Found matching items." 137 | } 138 | ``` 139 |
140 | 141 | 142 | ### Episodes of a podcast 143 | 144 | ```python 145 | results = index.episodesByFeedId(522613) 146 | results = index.episodesByFeedUrl("http://feed.thisamericanlife.org/talpodcast") 147 | results = index.episodesByItunesId(201671138) 148 | 149 | results = index.episodesByFeedId(522613, since=-525600) # in the last year 150 | results = index.episodesByFeedId(522613, since=1577836800) # Jan 1st 2020 151 | ``` 152 | 153 |
154 | Click to see sample result! 155 | 156 | ```javascript 157 | { 158 | "status": "true", 159 | "items": [ 160 | { 161 | "id": 1270106072, 162 | "title": "726: Twenty-Five", 163 | "link": "http://feed.thisamericanlife.org/~r/talpodcast/~3/p41tfsPlK00/twenty-five", 164 | "description": "To commemorate our show\u2019s 25th year, we have a program about people who were born the year our show went on\u00a0the\u00a0air.", 165 | "guid": "44678 at https://www.thisamericanlife.org", 166 | "datePublished": 1607900400, 167 | "datePublishedPretty": "December 13, 2020 5:00pm", 168 | "dateCrawled": 1607927945, 169 | "enclosureUrl": "https://www.podtrac.com/pts/redirect.mp3/podcast.thisamericanlife.org/podcast/726.mp3", 170 | "enclosureType": "audio/mpeg", 171 | "enclosureLength": 0, 172 | "duration": 3561, 173 | "explicit": 0, 174 | "episode": null, 175 | "episodeType": null, 176 | "season": 0, 177 | "image": "", 178 | "feedItunesId": 201671138, 179 | "feedImage": "https://files.thisamericanlife.org/sites/all/themes/thislife/img/tal-name-1400x1400.png", 180 | "feedId": 522613, 181 | "feedLanguage": "en", 182 | "chaptersUrl": null, 183 | "transcriptUrl": null 184 | }, 185 | ... 186 | ], 187 | "count": 28, 188 | "query": "201671138", 189 | "description": "Found matching items." 190 | } 191 | ``` 192 |
193 | 194 | 195 | ### Episode by ID 196 | 197 | ```python 198 | results = index.episodeById(1270106072) 199 | ``` 200 | 201 |
202 | Click to see sample result! 203 | 204 | ```javascript 205 | { 206 | "status": "true", 207 | "id": "1270106072", 208 | "episode": { 209 | "id": 1270106072, 210 | "title": "726: Twenty-Five", 211 | "link": "http://feed.thisamericanlife.org/~r/talpodcast/~3/p41tfsPlK00/twenty-five", 212 | "description": "To commemorate our show\u2019s 25th year, we have a program about people who were born the year our show went on\u00a0the\u00a0air.", 213 | "guid": "44678 at https://www.thisamericanlife.org", 214 | "datePublished": 1607900400, 215 | "datePublishedPretty": "December 13, 2020 5:00pm", 216 | "dateCrawled": 1607927945, 217 | "enclosureUrl": "https://www.podtrac.com/pts/redirect.mp3/podcast.thisamericanlife.org/podcast/726.mp3", 218 | "enclosureType": "audio/mpeg", 219 | "enclosureLength": 0, 220 | "duration": 3561, 221 | "explicit": 0, 222 | "episode": null, 223 | "episodeType": null, 224 | "season": 0, 225 | "image": "", 226 | "feedItunesId": 201671138, 227 | "feedImage": "https://files.thisamericanlife.org/sites/all/themes/thislife/img/tal-name-1400x1400.png", 228 | "feedId": 522613, 229 | "feedTitle": "This American Life", 230 | "feedLanguage": "en", 231 | "chaptersUrl": null, 232 | "transcriptUrl": null 233 | }, 234 | "description": "Found matching item." 235 | } 236 | ``` 237 |
238 | 239 | 240 | ### Recent episodes 241 | 242 | ```python 243 | results = index.recentEpisodes(max=5, excluding="trump", before_episode_id=1270106072) 244 | ``` 245 | 246 |
247 | Click to see sample result! 248 | 249 | ```javascript 250 | { 251 | "status": "true", 252 | "items": [ 253 | { 254 | "id": 1269804903, 255 | "title": "How epidemics and pandemics have changed history", 256 | "link": "http://www.abc.net.au/radionational/programs/rearvision/how-epidemics-and-pandemics-have-changed-history/12851986", 257 | "description": "Human history is usually understood through wars, economic changes, technological development or great leaders. What\u2019s frequently overlooked is the role of infectious disease epidemics and pandemics. But as the COVID-19 virus has reminded us, disease can change us in ways we could never imagine.", 258 | "guid": "http://www.abc.net.au/radionational/programs/rearvision/how-epidemics-and-pandemics-have-changed-history/12851986", 259 | "datePublished": 1608426300, 260 | "datePublishedPretty": "December 19, 2020 7:05pm", 261 | "dateCrawled": 1607923316, 262 | "enclosureUrl": "http://mpegmedia.abc.net.au/rn/podcast/2020/12/rvn_20201220.mp3", 263 | "enclosureType": "audio/mp3", 264 | "enclosureLength": 27955968, 265 | "explicit": 0, 266 | "episode": null, 267 | "episodeType": null, 268 | "season": 0, 269 | "image": "", 270 | "feedItunesId": 135114451, 271 | "feedImage": "http://www.abc.net.au/cm/rimage/9860262-1x1-thumbnail.jpg?v=2", 272 | "feedId": 990878, 273 | "feedTitle": "Rear Vision", 274 | "feedLanguage": "en-AU" 275 | }, 276 | ... 277 | ], 278 | "count": 5, 279 | "max": "5", 280 | "description": "Found matching items." 281 | } 282 | ``` 283 |
284 | 285 | ## Running the tests 286 | 287 | - Export the api key and secret 288 | 289 | ``` 290 | export PODCAST_INDEX_API_KEY="7B3U8VVP87QWSZUFXJRE" 291 | export PODCAST_INDEX_API_SECRET="4QwK83LA7RttCDdms9MnCn3HMYqGPG6CDEvnkL2w" 292 | ``` 293 | 294 | - Run the tests 295 | 296 | ``` 297 | coverage run -m pytest --log-cli-level=INFO 298 | ``` 299 | 300 | ## Contributing 301 | 302 | - Fork the repo 303 | - Add your awesome code 304 | - Submit a pull request 305 | - Ensure all existing tests pass 306 | - Bonus: include tests for your awesome new feature 307 | 308 | ## Updating the pip package 309 | 310 | This is mostly for myself since I have to lookup these commands every time ;) 311 | 312 | 1. Update version number in setup.py 313 | 2. Run the following commands 314 | ``` 315 | rm -rf build 316 | rm -rf dist 317 | python3 -m pip install --upgrade build 318 | python3 -m build 319 | ``` 320 | 3. Check that there is a .tar.gz and .whl file in the dist folder 321 | 4. Upload the new version 322 | ``` 323 | python3 -m pip install --upgrade twine 324 | twine upload dist/* 325 | ``` 326 | 327 | ## Support 328 | 329 | I am passionate about podcasting and work on this in my spare time. Hit me up and we can grab a virtual coffee together. 330 | 331 | Buy Me A Coffee 332 | -------------------------------------------------------------------------------- /podcastindex/podcastindex.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | import logging 4 | import os 5 | import time 6 | 7 | import requests 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | logger = logging.getLogger() 11 | 12 | 13 | def init(config): 14 | """ 15 | Create and return a new PodcastIndex object, initialized using the config. 16 | 17 | Args: 18 | config (Dict): Dictionary with 'api_key' and 'api_secret' keys. 19 | 20 | Returns: 21 | PodcastIndex: Initialized PodcastIndex object. 22 | """ 23 | return PodcastIndex(config) 24 | 25 | 26 | def get_config_from_env(): 27 | """ 28 | Retrieve the api key and secret from the envoronment. 29 | 30 | Raises: 31 | RuntimeError: If the key or secret is not found in the environment. 32 | 33 | Returns: 34 | Dict: config with "api_key" and "api_secret" 35 | """ 36 | api_key = os.getenv("PODCAST_INDEX_API_KEY") 37 | if api_key is None: 38 | error_msg = "Could not find PODCAST_INDEX_API_KEY environment variable" 39 | raise RuntimeError(error_msg) 40 | 41 | api_secret = os.environ.get("PODCAST_INDEX_API_SECRET") 42 | if api_secret is None: 43 | error_msg = "Could not find PODCAST_INDEX_API_SECRET environment variable" 44 | raise RuntimeError(error_msg) 45 | 46 | config = {"api_key": api_key, "api_secret": api_secret} 47 | return config 48 | 49 | 50 | class PodcastIndex: 51 | def __init__(self, config): 52 | assert "api_key" in config 53 | assert "api_secret" in config 54 | 55 | # Timeout used when making requests 56 | self.timeout = 5 57 | 58 | self.api_key = config["api_key"] 59 | self.api_secret = config["api_secret"] 60 | 61 | self.base_url = "https://api.podcastindex.org/api/1.0" 62 | 63 | def _create_headers(self): 64 | """ 65 | Hash the current timestamp along with the api key and secret to 66 | produce the headers for calling the api. 67 | 68 | Returns: 69 | dict: dictionary of header data 70 | """ 71 | # we'll need the unix time 72 | epoch_time = int(time.time()) 73 | 74 | # our hash here is the api key + secret + time 75 | data_to_hash = self.api_key + self.api_secret + str(epoch_time) 76 | 77 | # which is then sha-1'd 78 | sha_1 = hashlib.sha1(data_to_hash.encode()).hexdigest() 79 | 80 | # now we build our request headers 81 | headers = { 82 | "X-Auth-Date": str(epoch_time), 83 | "X-Auth-Key": self.api_key, 84 | "Authorization": sha_1, 85 | "User-Agent": "Voyce", 86 | } 87 | 88 | return headers 89 | 90 | def _make_request_get_result_helper(self, url, payload): 91 | """ 92 | Helper method DRY up the code. It performs the request and returns the result. 93 | 94 | Returns: 95 | [type]: [description] 96 | """ 97 | # Perform request 98 | headers = self._create_headers() 99 | result = requests.post(url, headers=headers, 100 | data=payload, timeout=self.timeout) 101 | result.raise_for_status() 102 | 103 | # Parse the result as a dict 104 | result_dict = json.loads(result.text) 105 | return result_dict 106 | 107 | def search(self, query, clean=False): 108 | """ 109 | Returns all of the feeds that match the search terms in the title, author or owner of the feed. 110 | 111 | Args: 112 | query (str): Query string 113 | clean (bool): Return only non-explicit feeds 114 | 115 | Raises: 116 | requests.exceptions.HTTPError: When the status code is not OK. 117 | requests.exceptions.ReadTimeout: When the request times out. 118 | 119 | Returns: 120 | Dict: API response 121 | """ 122 | # Setup request 123 | url = self.base_url + "/search/byterm" 124 | 125 | # Setup payload 126 | payload = {"q": query} 127 | if clean: 128 | payload["clean"] = 1 129 | 130 | # Call Api for result 131 | return self._make_request_get_result_helper(url, payload) 132 | 133 | def podcastByFeedUrl(self, feedUrl): 134 | """ 135 | Lookup a podcast by feedUrl. 136 | 137 | Args: 138 | feedUrl (string): The feed's url. 139 | 140 | Raises: 141 | requests.exceptions.HTTPError: When the status code is not OK. 142 | requests.exceptions.ReadTimeout: When the request times out. 143 | 144 | Returns: 145 | Dict: API response 146 | """ 147 | # Setup request 148 | url = self.base_url + "/podcasts/byfeedurl" 149 | 150 | # Setup payload 151 | payload = {"url": feedUrl} 152 | 153 | # Call Api for result 154 | return self._make_request_get_result_helper(url, payload) 155 | 156 | def podcastByFeedId(self, feedId): 157 | """ 158 | Lookup a podcast by feedId. 159 | 160 | Args: 161 | feedId (string or integer): Podcast index internal ID. 162 | 163 | Raises: 164 | requests.exceptions.HTTPError: When the status code is not OK. 165 | requests.exceptions.ReadTimeout: When the request times out. 166 | 167 | Returns: 168 | Dict: API response 169 | """ 170 | # Setup request 171 | url = self.base_url + "/podcasts/byfeedid" 172 | 173 | # Setup payload 174 | payload = {"id": feedId} 175 | 176 | # Call Api for result 177 | return self._make_request_get_result_helper(url, payload) 178 | 179 | def podcastByItunesId(self, itunesId): 180 | """ 181 | Lookup a podcast by itunesId. 182 | 183 | Args: 184 | itunesId (string or integer): Itunes ID for the feed. 185 | 186 | Raises: 187 | requests.exceptions.HTTPError: When the status code is not OK. 188 | requests.exceptions.ReadTimeout: When the request times out. 189 | 190 | Returns: 191 | Dict: API response 192 | """ 193 | # Setup request 194 | url = self.base_url + "/podcasts/byitunesid" 195 | 196 | # Setup payload 197 | payload = {"id": itunesId} 198 | 199 | # Call Api for result 200 | return self._make_request_get_result_helper(url, payload) 201 | 202 | def episodesByFeedUrl(self, feedUrl, since=None, max_results=10, fulltext=False): 203 | """ 204 | Lookup episodes by feedUrl, returned in reverse chronological order. 205 | 206 | Args: 207 | feedUrl (string or integer): The feed's url. 208 | since (integer): Unix timestamp, or a negative integer that represents a number of seconds prior to right 209 | now. The search will start from that time and only return feeds updated since then. 210 | max_results (integer): Maximum number of results to return. Default: 10 211 | fulltext (bool): Return full text in the text fields. Default: False 212 | 213 | Raises: 214 | requests.exceptions.HTTPError: When the status code is not OK. 215 | requests.exceptions.ReadTimeout: When the request times out. 216 | 217 | Returns: 218 | Dict: API response 219 | """ 220 | # Setup request 221 | url = self.base_url + "/episodes/byfeedurl" 222 | 223 | # Setup payload 224 | payload = {"url": feedUrl, "max": max_results} 225 | if since: 226 | payload["since"] = since 227 | if fulltext: 228 | payload["fulltext"] = True 229 | 230 | # Call Api for result 231 | return self._make_request_get_result_helper(url, payload) 232 | 233 | def podcastByGuid(self, guid): 234 | """ 235 | Lookup a podcast by guid. 236 | 237 | Args: 238 | guid (string): Podcast index guid. 239 | 240 | Raises: 241 | requests.exceptions.HTTPError: When the status code is not OK. 242 | requests.exceptions.ReadTimeout: When the request times out. 243 | 244 | Returns: 245 | Dict: API response 246 | """ 247 | # Setup request 248 | url = self.base_url + "/podcasts/byguid" 249 | 250 | # Setup payload 251 | payload = {"guid": guid} 252 | 253 | # Call Api for result 254 | return self._make_request_get_result_helper(url, payload) 255 | 256 | def episodesByFeedId( 257 | self, feedId, since=None, max_results=10, fulltext=False, enclosure=None 258 | ): 259 | """ 260 | Lookup episodes by feedId, returned in reverse chronological order. 261 | 262 | Args: 263 | feedId (string or integer): Podcast index internal ID. 264 | since (integer): Unix timestamp, or a negative integer that represents a number of seconds prior to right 265 | now. The search will start from that time and only return feeds updated since then. 266 | max_results (integer): Maximum number of results to return. Default: 10 267 | fulltext (bool): Return full text in the text fields. Default: False 268 | enclosure (string): The URL for the episode enclosure to get the information for. 269 | 270 | Raises: 271 | requests.exceptions.HTTPError: When the status code is not OK. 272 | requests.exceptions.ReadTimeout: When the request times out. 273 | 274 | Returns: 275 | Dict: API response 276 | """ 277 | # Setup request 278 | url = self.base_url + "/episodes/byfeedid" 279 | 280 | # Setup payload 281 | payload = {"id": feedId, "max": max_results} 282 | if since: 283 | payload["since"] = since 284 | if fulltext: 285 | payload["fulltext"] = True 286 | if enclosure: 287 | payload["enclosure"] = enclosure 288 | 289 | # Call Api for result 290 | return self._make_request_get_result_helper(url, payload) 291 | 292 | def episodesByItunesId( 293 | self, itunesId, since=None, max_results=10, fulltext=False, enclosure=None 294 | ): 295 | """ 296 | Lookup episodes by itunesId, returned in reverse chronological order. 297 | 298 | Args: 299 | itunesId (string or integer): Itunes ID for the feed. 300 | since (integer): Unix timestamp, or a negative integer that represents a number of seconds prior to right 301 | now. The search will start from that time and only return feeds updated since then. 302 | max_results (integer): Maximum number of results to return. Default: 10 303 | fulltext (bool): Return full text in the text fields. Default: False 304 | enclosure (string): The URL for the episode enclosure to get the information for. 305 | 306 | 307 | Raises: 308 | requests.exceptions.HTTPError: When the status code is not OK. 309 | requests.exceptions.ReadTimeout: When the request times out. 310 | 311 | Returns: 312 | Dict: API response 313 | """ 314 | # Setup request 315 | url = self.base_url + "/episodes/byitunesid" 316 | 317 | # Setup payload 318 | payload = { 319 | "id": itunesId, 320 | "max": max_results, 321 | "fulltext": True, 322 | } 323 | if since: 324 | payload["since"] = since 325 | if fulltext: 326 | payload["fulltext"] = True 327 | if enclosure: 328 | payload["enclosure"] = enclosure 329 | 330 | # Call Api for result 331 | return self._make_request_get_result_helper(url, payload) 332 | 333 | def episodesByPodcastGuid( 334 | self, podcastGuid, since=None, max_results=10, fulltext=False, enclosure=None 335 | ): 336 | """ 337 | Lookup episodes by podcast GUID, returned in reverse chronological order. 338 | 339 | Args: 340 | podcastGuid (string): Podcast index guid. 341 | since (integer): Unix timestamp, or a negative integer that represents a number of seconds prior to right 342 | now. The search will start from that time and only return feeds updated since then. 343 | max_results (integer): Maximum number of results to return. Default: 10 344 | fulltext (bool): Return full text in the text fields. Default: False 345 | enclosure (string): The URL for the episode enclosure to get the information for. 346 | 347 | Raises: 348 | requests.exceptions.HTTPError: When the status code is not OK. 349 | requests.exceptions.ReadTimeout: When the request times out. 350 | 351 | Returns: 352 | Dict: API response 353 | """ 354 | # Setup request 355 | url = self.base_url + "/episodes/bypodcastguid" 356 | 357 | # Setup payload 358 | payload = {"guid": podcastGuid, "max": max_results} 359 | if since: 360 | payload["since"] = since 361 | if fulltext: 362 | payload["fulltext"] = True 363 | if enclosure: 364 | payload["enclosure"] = enclosure 365 | 366 | # Call Api for result 367 | return self._make_request_get_result_helper(url, payload) 368 | 369 | def episodeById(self, id, fulltext=False): 370 | """ 371 | Lookup episode by id internal to podcast index. 372 | 373 | Args: 374 | id (string or integer): Episode ID. 375 | fulltext (bool): Return full text in the text fields. Default: False 376 | 377 | Raises: 378 | requests.exceptions.HTTPError: When the status code is not OK. 379 | requests.exceptions.ReadTimeout: When the request times out. 380 | 381 | Returns: 382 | Dict: API response 383 | """ 384 | # Setup request 385 | url = self.base_url + "/episodes/byid" 386 | 387 | # Setup payload 388 | payload = {"id": id} 389 | if fulltext: 390 | payload["fulltext"] = True 391 | 392 | # Call Api for result 393 | return self._make_request_get_result_helper(url, payload) 394 | 395 | def episodeByGuid( 396 | self, guid, feedurl=None, feedid=None, podcastguid=None, fulltext=False 397 | ): 398 | """ 399 | Lookup episode by guid. 400 | 401 | Args: 402 | guid (string): Episode GUID. 403 | feedurl (string): The feed's url. 404 | feedid (string or integer): Podcast index internal ID. 405 | podcastguid (string): The GUID of the podcast to search within. 406 | fulltext (bool): Return full text in the text fields. Default: False 407 | 408 | *guid and at least one of feedurl or feedid must be specified. 409 | 410 | Raises: 411 | requests.exceptions.HTTPError: When the status code is not OK. 412 | requests.exceptions.ReadTimeout: When the request times out. 413 | 414 | Returns: 415 | Dict: API response 416 | """ 417 | if not guid: 418 | raise ValueError("guid must not be None or empty") 419 | if not (feedurl or feedid or podcastguid): 420 | raise ValueError( 421 | "At least one of feedurl or feedid or podcastguid must not be None or empty" 422 | ) 423 | 424 | # Setup request 425 | url = self.base_url + "/episodes/byguid" 426 | 427 | # Setup payload 428 | payload = {"guid": guid} 429 | if feedurl: 430 | payload["feedurl"] = feedurl 431 | if feedid: 432 | payload["feedid"] = feedid 433 | if podcastguid: 434 | payload["podcastguid"] = podcastguid 435 | if fulltext: 436 | payload["fulltext"] = True 437 | 438 | # Call Api for result 439 | return self._make_request_get_result_helper(url, payload) 440 | 441 | def episodesByPerson(self, query, clean=False, fulltext=False): 442 | """ 443 | Returns all of the episodes where the specified person is mentioned. 444 | 445 | Args: 446 | query (str): Query string 447 | clean (bool): Return only non-explicit feeds 448 | fulltext (bool): Return full text in the text fields. Default: False 449 | 450 | Raises: 451 | requests.exceptions.HTTPError: When the status code is not OK. 452 | requests.exceptions.ReadTimeout: When the request times out. 453 | 454 | Returns: 455 | Dict: API response 456 | """ 457 | # Setup request 458 | url = self.base_url + "/search/byperson" 459 | 460 | # Setup payload 461 | payload = {"q": query} 462 | if clean: 463 | payload["clean"] = 1 464 | if fulltext: 465 | payload["fulltext"] = True 466 | 467 | # Call Api for result 468 | return self._make_request_get_result_helper(url, payload) 469 | 470 | def randomEpisodes(self, max=None, lang=None, cat=None, notcat=None, fulltext=False): 471 | """ 472 | Fetch a random batch of episodes, in no specific order. 473 | See https://podcastindex-org.github.io/docs-api/#get-/episodes/random 474 | for more information and examples on how to specify the arguments below. 475 | 476 | Args: 477 | max (int): Maximum number of episodes to return. Default: 1 478 | lang (str): Language code to filter by. 479 | cat (str): Specify that you ONLY want episodes with these categories in the results. 480 | notcat (str): Specify categories of episodes to NOT show in the results. 481 | fulltext (bool): Return full text in the text fields. Default: False 482 | 483 | Raises: 484 | requests.exceptions.HTTPError: When the status code is not OK. 485 | requests.exceptions.ReadTimeout: When the request times out. 486 | 487 | Returns: 488 | Dict: API response 489 | """ 490 | # Setup request 491 | url = self.base_url + "/episodes/random" 492 | 493 | # Setup payload 494 | payload = {"pretty": 1} 495 | if max: 496 | payload["max"] = max 497 | if lang: 498 | payload["lang"] = lang 499 | if cat: 500 | payload["cat"] = cat 501 | if notcat: 502 | payload["notcat"] = notcat 503 | if fulltext: 504 | payload["fulltext"] = True 505 | 506 | # Call Api for result 507 | return self._make_request_get_result_helper(url, payload) 508 | 509 | 510 | def recentEpisodes( 511 | self, max=None, excluding=None, before_episode_id=None, fulltext=False 512 | ): 513 | """ 514 | Returns the most recent [max] number of episodes globally across the whole index, in reverse chronological 515 | order. 516 | 517 | Args: 518 | max (int, optional): Maximum number of results to return. 519 | excluding ([type], optional): Any item containing this string in the title or url will be discarded from 520 | the result set. 521 | before_episode_id (int, optional): Get recent episodes before this episode id, allowing you to walk back 522 | through the episode history sequentially. 523 | fulltext (bool): Return full text in the text fields. Default: False 524 | 525 | Raises: 526 | requests.exceptions.HTTPError: When the status code is not OK. 527 | requests.exceptions.ReadTimeout: When the request times out. 528 | 529 | Returns: 530 | Dict: API response 531 | """ 532 | # Setup request 533 | url = self.base_url + "/recent/episodes" 534 | 535 | # Setup payload 536 | payload = {} 537 | if max: 538 | payload["max"] = max 539 | if excluding: 540 | payload["excludeString"] = excluding 541 | if before_episode_id: 542 | payload["before"] = before_episode_id 543 | if fulltext: 544 | payload["fulltext"] = True 545 | 546 | # Call Api for result 547 | return self._make_request_get_result_helper(url, payload) 548 | 549 | def recentFeeds( 550 | self, max=40, since=None, lang=None, categories=None, not_categories=None 551 | ): 552 | """ 553 | Returns the most recent [max] feeds, in reverse chronological order. 554 | 555 | Args: 556 | max (int, optional): Maximum number of results to return. Default: 40 557 | since (int): Return items since the specified time. Can be a unix epoch timestamp or a negative integer 558 | that represents a number of seconds prior to right now 559 | lang ([string], optional): Specifying a language code will return podcasts only in that language. 560 | categories ([string or int], optional): A list of categories used to limit which podcasts will be included 561 | in results. Category names and IDs are both supported. 562 | not_categories ([string or int], optional): A list of categories used to limit exclude certain podcasts 563 | from results. Category names and IDs are both supported. 564 | 565 | Raises: 566 | requests.exceptions.HTTPError: When the status code is not OK. 567 | requests.exceptions.ReadTimeout: When the request times out. 568 | 569 | Returns: 570 | Dict: API response 571 | """ 572 | # Setup request 573 | url = self.base_url + "/recent/feeds" 574 | 575 | # Setup payload 576 | payload = {} 577 | if max: 578 | payload["max"] = max 579 | if since: 580 | payload["since"] = since 581 | if lang: 582 | payload["lang"] = ",".join(str(i) for i in lang) 583 | if categories: 584 | payload["cat"] = ",".join(str(i) for i in categories) 585 | if not_categories: 586 | payload["notcat"] = ",".join(str(i) for i in not_categories) 587 | 588 | # Call Api for result 589 | return self._make_request_get_result_helper(url, payload) 590 | 591 | def newFeeds(self, max=40, since=None, feed_id=None, desc=None): 592 | """ 593 | Returns every new feed added to the index over the past 24 hours in reverse chronological order. 594 | 595 | Args: 596 | max (int, optional): Maximum number of results to return. Default: 40 597 | since (int): Return items since the specified time. Can be a unix epoch timestamp or a negative integer 598 | that represents a number of seconds prior to right now 599 | feed_id (string or int): The PodcastIndex Feed ID to start from (or go to if desc specified). 600 | If since parameter also specified, value of since is ignored. 601 | desc (bool): If true, return results in descending order. Only applicable when using feedid parameter. 602 | Default: False 603 | 604 | Raises: 605 | requests.exceptions.HTTPError: When the status code is not OK. 606 | requests.exceptions.ReadTimeout: When the request times out. 607 | 608 | Returns: 609 | Dict: API response 610 | """ 611 | # Setup request 612 | url = self.base_url + "/recent/feeds" 613 | 614 | # Setup payload 615 | payload = {} 616 | if max: 617 | payload["max"] = max 618 | if since: 619 | payload["since"] = since 620 | if feed_id: 621 | payload["feedid"] = feed_id 622 | if desc: 623 | payload["desc"] = desc 624 | 625 | # Call Api for result 626 | return self._make_request_get_result_helper(url, payload) 627 | 628 | def trendingPodcasts( 629 | self, max=10, since=None, lang=None, categories=None, not_categories=None 630 | ): 631 | """ 632 | Returns the podcasts in the index that are trending. 633 | 634 | Args: 635 | max (int): Maximum number of results to return. Default: 10 636 | since (int): Return items since the specified time. Can be a unix epoch timestamp or a negative integer 637 | that represents a number of seconds prior to right now 638 | lang ([string], optional): Specifying a language code will return podcasts only in that language. 639 | categories ([string or int], optional): A list of categories used to limit which podcasts will be included 640 | in results. Category names and IDs are both supported. 641 | not_categories ([string or int], optional): A list of categories used to limit exclude certain podcasts 642 | from results. Category names and IDs are both supported. 643 | """ 644 | # Setup request 645 | url = self.base_url + "/podcasts/trending" 646 | 647 | # Setup payload 648 | payload = {} 649 | if max: 650 | payload["max"] = max 651 | if since: 652 | payload["since"] = since 653 | if lang: 654 | payload["lang"] = ",".join(str(i) for i in lang) 655 | if categories: 656 | payload["cat"] = ",".join(str(i) for i in categories) 657 | if not_categories: 658 | payload["notcat"] = ",".join(str(i) for i in not_categories) 659 | 660 | # Call Api for result 661 | return self._make_request_get_result_helper(url, payload) 662 | 663 | def addByItunesId(self, itunesId): 664 | """ 665 | Request a podcast be added to the index based on itunesId. 666 | 667 | Args: 668 | itunesId (string or integer): Itunes ID for the feed. 669 | 670 | Raises: 671 | requests.exceptions.HTTPError: When the status code is not OK. 672 | requests.exceptions.ReadTimeout: When the request times out. 673 | 674 | Returns: 675 | Dict: API response 676 | """ 677 | # Setup request 678 | url = self.base_url + "/add/byitunesid" 679 | 680 | # Setup payload 681 | payload = { 682 | "id": itunesId, 683 | "pretty": 1, 684 | } 685 | 686 | # Call Api for result 687 | return self._make_request_get_result_helper(url, payload) 688 | 689 | def pubNotifyUpdate(self, id): 690 | """ 691 | Request a podcast be updated. 692 | 693 | Args: 694 | id (string or integer): ID of the podcast to update. 695 | 696 | Raises: 697 | requests.exceptions.HTTPError: When the status code is not OK. 698 | requests.exceptions.ReadTimeout: When the request times out. 699 | 700 | Returns: 701 | Dict: API response 702 | """ 703 | # Setup request 704 | url = self.base_url + "/hub/pubnotify" 705 | 706 | # Setup payload 707 | payload = { 708 | "id": id, 709 | "pretty": 1, 710 | } 711 | 712 | # Call Api for result 713 | return self._make_request_get_result_helper(url, payload) 714 | --------------------------------------------------------------------------------