Skip to content

Commit 19a4448

Browse files
authored
Merge pull request #3 from ListenNotes/fix/delete-playlist
Add delete_playlist support for Python SDK 3.1.0
2 parents fd78aa6 + 522e14f commit 19a4448

7 files changed

Lines changed: 101 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ And you can see some sample code [here](https://github.com/ListenNotes/podcast-a
119119
- [`fetch_podcasts_by_domain`](#fetch_podcasts_by_domain) — `GET /podcasts/domains/{domain_name}`
120120
- [`create_playlist`](#create_playlist) — `POST /playlists`
121121
- [`update_playlist`](#update_playlist) — `PUT /playlists/{id}`
122+
- [`delete_playlist`](#delete_playlist) — `DELETE /playlists/{id}`
122123
- [`add_playlist_item`](#add_playlist_item) — `POST /playlists/{id}/items`
123124
- [`delete_playlist_item`](#delete_playlist_item) — `DELETE /playlists/{id}/items/{item_id}`
124125
- [`update_playlist_item_notes`](#update_playlist_item_notes) — `PUT /playlists/{id}/items/{item_id}`
@@ -720,6 +721,29 @@ print(response.json())
720721

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

724+
### delete_playlist
725+
726+
Delete a playlist.
727+
728+
`DELETE /playlists/{id}`
729+
730+
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.
731+
732+
**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.**
733+
734+
Only playlists owned by your admin API account can be modified; contributor membership does not grant write access.
735+
736+
```python
737+
import os
738+
from listennotes import podcast_api
739+
740+
client = podcast_api.Client(api_key=os.environ.get("LISTEN_API_KEY"))
741+
response = client.delete_playlist(**{'id': 'm1pe7z60bsw'})
742+
print(response.json())
743+
```
744+
745+
[Full API documentation](https://www.listennotes.com/api/docs/#delete-api-v2-playlists-id)
746+
723747
### add_playlist_item
724748

725749
Add an episode or podcast to a playlist.

‎listennotes/_api_methods.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,14 @@ def update_playlist(self, **kwargs):
250250
kwargs,
251251
)
252252

253+
def delete_playlist(self, **kwargs):
254+
return self._request_api(
255+
"DELETE",
256+
"/playlists/{id}",
257+
(),
258+
kwargs,
259+
)
260+
253261
def add_playlist_item(self, **kwargs):
254262
return self._request_api(
255263
"POST",

‎listennotes/api-contract.json‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schema_version": 1,
3-
"version": "3.0.0",
3+
"version": "3.1.0",
44
"operations": [
55
{
66
"operationId": "search",
@@ -815,6 +815,25 @@
815815
"summary": "Update playlist metadata.",
816816
"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."
817817
},
818+
{
819+
"operationId": "deletePlaylist",
820+
"func": "delete_playlist",
821+
"available_from": "3.1.0",
822+
"method": "DELETE",
823+
"path": "/playlists/{id}",
824+
"parameters": [
825+
{
826+
"name": "id",
827+
"in": "path",
828+
"required": true
829+
}
830+
],
831+
"example_params": {
832+
"id": "m1pe7z60bsw"
833+
},
834+
"summary": "Delete a playlist.",
835+
"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."
836+
},
818837
{
819838
"operationId": "addPlaylistItem",
820839
"func": "add_playlist_item",

‎listennotes/version.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "3.0.0"
1+
VERSION = "3.1.0"

‎tests/test_client.py‎

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import requests
88

99
from listennotes import podcast_api
10+
from listennotes.version import VERSION
1011

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

9293

93-
def test_delete_podcast_reason_is_query_not_body(transport):
94-
podcast_api.Client().delete_podcast(id="abc", reason="a & b")
94+
@pytest.mark.parametrize(
95+
"method,params,path",
96+
[
97+
(
98+
"delete_podcast",
99+
{"id": "abc", "reason": "a & b"},
100+
"/podcasts/abc?reason=a+%26+b",
101+
),
102+
(
103+
"delete_playlist",
104+
{"id": "a/b ?#%"},
105+
"/playlists/a%2Fb%20%3F%23%25",
106+
),
107+
],
108+
)
109+
def test_delete_encodes_path_and_query_without_body(
110+
method, params, path, transport
111+
):
112+
getattr(podcast_api.Client(), method)(**params)
95113
request = transport.calls[0][0]
96114
assert request.method == "DELETE"
97-
assert request.url.endswith("/podcasts/abc?reason=a+%26+b")
115+
assert request.url == podcast_api.api_base_test + path
98116
assert request.body is None
99117

100118

@@ -157,13 +175,25 @@ def test_metadata_clearing_and_type(transport):
157175
}
158176

159177

160-
@pytest.mark.parametrize("field", ["id", "item_id"])
178+
@pytest.mark.parametrize(
179+
"method,params,field",
180+
[
181+
("delete_playlist", {}, "id"),
182+
("delete_playlist_item", {"id": "playlist", "item_id": 23}, "id"),
183+
(
184+
"delete_playlist_item",
185+
{"id": "playlist", "item_id": 23},
186+
"item_id",
187+
),
188+
],
189+
)
161190
@pytest.mark.parametrize("value", [None, ""])
162-
def test_missing_path_parameter_fails_before_http(field, value, transport):
163-
params = {"id": "playlist", "item_id": 23}
164-
params[field] = value
191+
def test_missing_path_parameter_fails_before_http(
192+
method, params, field, value, transport
193+
):
194+
params = {**params, field: value}
165195
with pytest.raises(ValueError, match=f"Missing path parameter: {field}"):
166-
podcast_api.Client().delete_playlist_item(**params)
196+
getattr(podcast_api.Client(), method)(**params)
167197
assert not transport.calls
168198

169199

@@ -182,7 +212,7 @@ def test_clients_keep_independent_keys_sessions_and_configuration(transport):
182212
assert transport.calls[0][0].headers["User-Agent"] == "first-agent"
183213
assert (
184214
transport.calls[1][0].headers["User-Agent"]
185-
== "podcast-api-python 3.0.0"
215+
== f"podcast-api-python {VERSION}"
186216
)
187217
assert first.http_client.session is not second.http_client.session
188218
assert (

‎tests/test_http_utils.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def test_exception_without_message_is_printable():
173173
("fetch_my_playlists", {}, 4),
174174
("create_playlist", {"name": "test"}, 1),
175175
("update_playlist", {"id": "playlist", "description": ""}, 1),
176+
("delete_playlist", {"id": "playlist"}, 1),
176177
("delete_playlist_item", {"id": "playlist", "item_id": 23}, 1),
177178
],
178179
)

‎tests/test_integration.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ def test_delete_playlist_item(client):
173173
assert response.request.body is None
174174

175175

176+
def test_delete_playlist(client):
177+
response = client.delete_playlist(id=PLAYLIST_ID)
178+
payload = response_json(response, "DELETE", f"/playlists/{PLAYLIST_ID}")
179+
assert payload == {"id": PLAYLIST_ID, "deleted": True}
180+
assert urlparse(response.request.url).query == ""
181+
assert response.request.body is None
182+
183+
176184
def test_missing_route(client):
177185
with pytest.raises(NotFoundError) as exc:
178186
client.http_client.get(MOCK_BASE + "/sdk-integration-missing-route")

0 commit comments

Comments
 (0)