Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ And you can see some sample code [here](https://github.com/ListenNotes/podcast-a
- [`fetch_podcasts_by_domain`](#fetch_podcasts_by_domain) — `GET /podcasts/domains/{domain_name}`
- [`create_playlist`](#create_playlist) — `POST /playlists`
- [`update_playlist`](#update_playlist) — `PUT /playlists/{id}`
- [`delete_playlist`](#delete_playlist) — `DELETE /playlists/{id}`
- [`add_playlist_item`](#add_playlist_item) — `POST /playlists/{id}/items`
- [`delete_playlist_item`](#delete_playlist_item) — `DELETE /playlists/{id}/items/{item_id}`
- [`update_playlist_item_notes`](#update_playlist_item_notes) — `PUT /playlists/{id}/items/{item_id}`
Expand Down Expand Up @@ -720,6 +721,29 @@ print(response.json())

[Full API documentation](https://www.listennotes.com/api/docs/#put-api-v2-playlists-id)

### delete_playlist

Delete a playlist.

`DELETE /playlists/{id}`

Permanently delete a playlist, including all episode and podcast references saved in this specific playlist and their notes. The actual episodes and podcasts remain in the Listen Notes podcast database.

**Warning: Deletion cannot be undone. Once deleted, the playlist is gone, regardless of how many episodes or podcasts it contains. You, the developer, are responsible for adding a confirmation step in your app's UI before calling this endpoint to prevent accidental deletion.**

Only playlists owned by your admin API account can be modified; contributor membership does not grant write access.

```python
import os
from listennotes import podcast_api

client = podcast_api.Client(api_key=os.environ.get("LISTEN_API_KEY"))
response = client.delete_playlist(**{'id': 'm1pe7z60bsw'})
print(response.json())
```

[Full API documentation](https://www.listennotes.com/api/docs/#delete-api-v2-playlists-id)

### add_playlist_item

Add an episode or podcast to a playlist.
Expand Down
8 changes: 8 additions & 0 deletions listennotes/_api_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,14 @@ def update_playlist(self, **kwargs):
kwargs,
)

def delete_playlist(self, **kwargs):
return self._request_api(
"DELETE",
"/playlists/{id}",
(),
kwargs,
)

def add_playlist_item(self, **kwargs):
return self._request_api(
"POST",
Expand Down
21 changes: 20 additions & 1 deletion listennotes/api-contract.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"version": "3.0.0",
"version": "3.1.0",
"operations": [
{
"operationId": "search",
Expand Down Expand Up @@ -815,6 +815,25 @@
"summary": "Update playlist metadata.",
"description": "Update any subset of name, description, visibility, and type. Omitted fields remain unchanged; at least one field is required. Switching to private rotates the playlist RSS secret. Type selects the saved default view (episode_list or podcast_list) and the returned listennotes_url; changing it preserves all existing episodes and podcasts.\n\nOnly playlists owned by your admin API account can be modified; contributor membership does not grant write access."
},
{
"operationId": "deletePlaylist",
"func": "delete_playlist",
"available_from": "3.1.0",
"method": "DELETE",
"path": "/playlists/{id}",
"parameters": [
{
"name": "id",
"in": "path",
"required": true
}
],
"example_params": {
"id": "m1pe7z60bsw"
},
"summary": "Delete a playlist.",
"description": "Permanently delete a playlist, including all episode and podcast references saved in this specific playlist and their notes. The actual episodes and podcasts remain in the Listen Notes podcast database.\n\n**Warning: Deletion cannot be undone. Once deleted, the playlist is gone, regardless of how many episodes or podcasts it contains. You, the developer, are responsible for adding a confirmation step in your app's UI before calling this endpoint to prevent accidental deletion.**\n\nOnly playlists owned by your admin API account can be modified; contributor membership does not grant write access."
},
{
"operationId": "addPlaylistItem",
"func": "add_playlist_item",
Expand Down
2 changes: 1 addition & 1 deletion listennotes/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "3.0.0"
VERSION = "3.1.0"
48 changes: 39 additions & 9 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import requests

from listennotes import podcast_api
from listennotes.version import VERSION

CONTRACT = json.loads(
files("listennotes").joinpath("api-contract.json").read_text()
Expand Down Expand Up @@ -90,11 +91,28 @@ def test_query_encoding_and_forward_compatible_fields(transport):
assert request.url.startswith(podcast_api.api_base_test)


def test_delete_podcast_reason_is_query_not_body(transport):
podcast_api.Client().delete_podcast(id="abc", reason="a & b")
@pytest.mark.parametrize(
"method,params,path",
[
(
"delete_podcast",
{"id": "abc", "reason": "a & b"},
"/podcasts/abc?reason=a+%26+b",
),
(
"delete_playlist",
{"id": "a/b ?#%"},
"/playlists/a%2Fb%20%3F%23%25",
),
],
)
def test_delete_encodes_path_and_query_without_body(
method, params, path, transport
):
getattr(podcast_api.Client(), method)(**params)
request = transport.calls[0][0]
assert request.method == "DELETE"
assert request.url.endswith("/podcasts/abc?reason=a+%26+b")
assert request.url == podcast_api.api_base_test + path
assert request.body is None


Expand Down Expand Up @@ -157,13 +175,25 @@ def test_metadata_clearing_and_type(transport):
}


@pytest.mark.parametrize("field", ["id", "item_id"])
@pytest.mark.parametrize(
"method,params,field",
[
("delete_playlist", {}, "id"),
("delete_playlist_item", {"id": "playlist", "item_id": 23}, "id"),
(
"delete_playlist_item",
{"id": "playlist", "item_id": 23},
"item_id",
),
],
)
@pytest.mark.parametrize("value", [None, ""])
def test_missing_path_parameter_fails_before_http(field, value, transport):
params = {"id": "playlist", "item_id": 23}
params[field] = value
def test_missing_path_parameter_fails_before_http(
method, params, field, value, transport
):
params = {**params, field: value}
with pytest.raises(ValueError, match=f"Missing path parameter: {field}"):
podcast_api.Client().delete_playlist_item(**params)
getattr(podcast_api.Client(), method)(**params)
assert not transport.calls


Expand All @@ -182,7 +212,7 @@ def test_clients_keep_independent_keys_sessions_and_configuration(transport):
assert transport.calls[0][0].headers["User-Agent"] == "first-agent"
assert (
transport.calls[1][0].headers["User-Agent"]
== "podcast-api-python 3.0.0"
== f"podcast-api-python {VERSION}"
)
assert first.http_client.session is not second.http_client.session
assert (
Expand Down
1 change: 1 addition & 0 deletions tests/test_http_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def test_exception_without_message_is_printable():
("fetch_my_playlists", {}, 4),
("create_playlist", {"name": "test"}, 1),
("update_playlist", {"id": "playlist", "description": ""}, 1),
("delete_playlist", {"id": "playlist"}, 1),
("delete_playlist_item", {"id": "playlist", "item_id": 23}, 1),
],
)
Expand Down
8 changes: 8 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ def test_delete_playlist_item(client):
assert response.request.body is None


def test_delete_playlist(client):
response = client.delete_playlist(id=PLAYLIST_ID)
payload = response_json(response, "DELETE", f"/playlists/{PLAYLIST_ID}")
assert payload == {"id": PLAYLIST_ID, "deleted": True}
assert urlparse(response.request.url).query == ""
assert response.request.body is None


def test_missing_route(client):
with pytest.raises(NotFoundError) as exc:
client.http_client.get(MOCK_BASE + "/sdk-integration-missing-route")
Expand Down
Loading