├── 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 |
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 |
--------------------------------------------------------------------------------