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
2 changes: 1 addition & 1 deletion .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
sdk_version=$(ruby -r ./lib/version -e 'print PodcastApi::VERSION')
gem install "pkg/podcast_api-$sdk_version.gem" --no-document
cd /tmp
ruby -r podcast_api -e 'abort unless PodcastApi::VERSION == ARGV.fetch(0) && PodcastApi::Client.new.respond_to?(:add_playlist_item)' "$sdk_version"
ruby -r podcast_api -e 'abort unless PodcastApi::VERSION == ARGV.fetch(0) && PodcastApi::Client.new.respond_to?(:delete_playlist)' "$sdk_version"
integration:
name: Integration (public mock API)
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
podcast_api (3.0.0)
podcast_api (3.1.0)
httparty (>= 0.24.1, < 0.25)

GEM
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ If you have any questions, please contact [hello@listennotes.com](hello@listenno
- [`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 @@ -791,6 +792,30 @@ puts response.parsed_response

[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.

```ruby
require "podcast_api"

client = PodcastApi::Client.new(api_key: ENV["LISTEN_API_KEY"])
response = client.delete_playlist(**{
'id' => 'm1pe7z60bsw'
})
puts response.parsed_response
```

[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
21 changes: 20 additions & 1 deletion lib/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
5 changes: 5 additions & 0 deletions lib/api_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ def update_playlist(**kwargs)
[], kwargs)
end

def delete_playlist(**kwargs)
request_api('DELETE', '/playlists/{id}',
[], kwargs)
end

def add_playlist_item(**kwargs)
request_api('POST', '/playlists/{id}/items',
[], kwargs)
Expand Down
2 changes: 1 addition & 1 deletion lib/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module PodcastApi
VERSION = '3.0.0'
VERSION = '3.1.0'
end
4 changes: 4 additions & 0 deletions tests/integration/mock_api_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ def assert_response(response, method, path, status = 200)
elsif operation['operationId'] == 'deletePlaylistItem'
assert_equal true, payload.fetch('deleted')
assert_kind_of Integer, payload.fetch('id')
elsif operation['operationId'] == 'deletePlaylist'
assert_equal true, payload.fetch('deleted')
assert_equal operation.fetch('example_params').fetch('id'), payload.fetch('id')
assert_nil response.request.options[:body]
end
end
end
Expand Down
51 changes: 42 additions & 9 deletions tests/podcast_api_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,28 @@ def test_omitted_notes_are_not_sent
assert_requested stub, times: 1
end

def test_delete_playlist_encodes_id_without_query_or_body
params = {id: 'a/b ?#%+é'}
before = params.dup
stub = stub_request(:delete, MOCK + '/playlists/a%2Fb%20%3F%23%25%2B%C3%A9')
.with(query: {}).with { |req| req.body.nil? || req.body.empty? }
.to_return(body: JSON.generate(id: params[:id], deleted: true),
headers: {'Content-Type' => 'application/json'})
response = @client.delete_playlist(**params)
assert_kind_of HTTParty::Response, response
assert_equal({'id' => params[:id], 'deleted' => true}, response.parsed_response)
assert_equal before, params
assert_nil response.request.options[:body]
assert_false response.request.options[:headers].key?('Content-Type')
assert_requested stub, times: 1
end

def test_missing_path_parameters_fail_before_network_access
[{}, {id: nil}, {id: ''}].each do |params|
error = assert_raise(PodcastApi::InvalidRequestError) { @client.delete_playlist(**params) }
assert_equal 'Missing required path parameter: id', error.message
assert_nil error.response
end
[{}, {id: nil}, {id: ''}, {id: 'abc'}, {id: 'abc', item_id: nil}, {id: 'abc', item_id: ''}].each do |params|
error = assert_raise(PodcastApi::InvalidRequestError) { @client.delete_playlist_item(**params) }
assert_match(/Missing required path parameter:/, error.message)
Expand Down Expand Up @@ -134,14 +155,17 @@ def test_invalid_timeouts
403 => PodcastApi::PermissionDeniedError, 404 => PodcastApi::NotFoundError,
429 => PodcastApi::RateLimitError, 500 => PodcastApi::PodcastApiError}.each do |status, klass|
define_method("test_error_#{status}_preserves_server_message_and_response") do
stub = stub_request(:post, MOCK + '/playlists').to_return(
status: status, body: '{"error":"Specific API error"}', headers: {'X-ListenAPI-Usage' => '5'}
)
error = assert_raise(klass) { @client.create_playlist(name: 'test') }
assert_match(/Specific API error/, error.message)
assert_equal status, error.response.code
assert_equal '5', error.response.headers['X-ListenAPI-Usage']
assert_requested stub, times: 1
[[:post, '/playlists', :create_playlist, {name: 'test'}],
[:delete, '/playlists/abc', :delete_playlist, {id: 'abc'}]].each do |method, path, func, params|
stub = stub_request(method, MOCK + path).to_return(
status: status, body: '{"error":"Specific API error"}', headers: {'X-ListenAPI-Usage' => '5'}
)
error = assert_raise(klass) { @client.public_send(func, **params) }
assert_match(/Specific API error/, error.message)
assert_equal status, error.response.code
assert_equal '5', error.response.headers['X-ListenAPI-Usage']
assert_requested stub, times: 1
end
end
end

Expand All @@ -164,11 +188,17 @@ def test_all_success_statuses_keep_response_envelope

def test_redirect_is_not_followed
[301, 302, 303, 307, 308].each do |status|
WebMock.reset!
stub_request(:post, MOCK + '/playlists').to_return(status: status, headers: {'Location' => PROD + '/playlists'})
error = assert_raise(PodcastApi::PodcastApiError) { @client.create_playlist(name: 'test') }
assert_equal status, error.response.code
stub = stub_request(:delete, MOCK + '/playlists/abc')
.to_return(status: status, headers: {'Location' => PROD + '/playlists/abc'})
error = assert_raise(PodcastApi::PodcastApiError) { @client.delete_playlist(id: 'abc') }
assert_equal status, error.response.code
assert_requested stub, times: 1
assert_not_requested :any, /listen-api\.listennotes\.com/
end
assert_not_requested :any, /listen-api\.listennotes\.com/
end

def test_connection_failures_are_wrapped_without_retries
Expand All @@ -178,6 +208,9 @@ def test_connection_failures_are_wrapped_without_retries
stub = stub_request(:put, MOCK + '/playlists/abc').to_raise(klass.new('connection failed'))
assert_raise(PodcastApi::APIConnectionError) { @client.update_playlist(id: 'abc', description: '') }
assert_requested stub, times: 1
stub = stub_request(:delete, MOCK + '/playlists/abc').to_raise(klass.new('connection failed'))
assert_raise(PodcastApi::APIConnectionError) { @client.delete_playlist(id: 'abc') }
assert_requested stub, times: 1
end
end

Expand Down
Loading