Skip to content
Draft
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
22 changes: 21 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export default defineConfig({
starlight({
title: "Open Podcast API",
favicon: "favicon.ico",
customCss: [
'./src/styles/custom.css',
],
social: [
{
icon: "github",
Expand Down Expand Up @@ -47,11 +50,26 @@ export default defineConfig({
},
{
label: "Subscriptions",
badge: {
text: "Core",
variant: "caution",
},
collapsed: true,
autogenerate: {
directory: "specs/subscriptions",
},
},
{
label: "Episodes",
badge: {
text: "Core",
variant: "caution",
},
collapsed: true,
autogenerate: {
directory: "specs/episodes",
},
},
],
},
...openAPISidebarGroups,
Expand All @@ -68,7 +86,9 @@ export default defineConfig({
"TabItem",
],
},
"src/components/SponsorCallout.astro"
"src/components/SponsorCallout.astro",
"src/components/BadgeOptional.astro",
"src/components/BadgeCore.astro"
],
}),
],
Expand Down
213 changes: 211 additions & 2 deletions schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ info:
tags:
- name: Subscriptions
description: All actions relating to subscription management
- name: Episodes
description: All actions relating to episode management
paths:
/subscriptions:
get:
Expand Down Expand Up @@ -202,6 +204,51 @@ paths:
security:
- podcast_auth:
- read:subscriptions
/episodes:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sporiff Could I ask you to review? I tried to follow the format of the existing file :-)

get:
tags:
- Episodes
summary: Retrieve all episodes for the authenticated user
description: Retrieve all episodes that has changed for the authenticated user since the provided timestamp
operationId: getEpisodes
parameters:
- in: query
name: since
schema:
type: string
format: date-time
required: false
example:
'2022-04-23T18:25:43.511Z'
- in: query
name: page
schema:
type: number
required: false
example:
1
- in: query
name: per_page
schema:
type: number
required: false
example:
5
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Episodes'
application/xml:
schema:
$ref: '#/components/schemas/Episodes'
'401':
$ref: '#/components/responses/Unauthorized'
security:
- podcast_auth:
- read:subscriptions
components:
responses:
Unauthorized:
Expand Down Expand Up @@ -357,6 +404,7 @@ components:
guid:
type: string
format: guid
description: The unique identifier (guid) for the subscription, as declared in the feed or generated by the server
is_subscribed:
type: boolean
subscription_changed:
Expand Down Expand Up @@ -550,6 +598,167 @@ components:
subscription_changed: 2023-02-23T14:41:00.000Z
guid_changed: 2023-02-23T14:41:00.000Z
new_guid: 965fcecf-ce04-482b-b57c-3119b866cc61
Episode:
xml:
name: episode
required:
- podcast_guid
- sync_id
- episode_guid
- title
- publish_date
- enclosure_url
- episode_url
type: object
properties:
podcast_guid:
type: string
format: guid
sync_id:
type: string
format: guid
episode_guid:
type: string
title:
type: string
publish_date:
type: string
format: date-time
enclosure_url:
type: string
format: url
episode_url:
type: string
format: url
playback_position:
value:
type: number
format: integer
timestamp:
type: string
format: date-time
played_status:
value:
type: boolean
timestamp:
type: string
format: date-time
new_status:
value:
type: boolean
timestamp:
type: string
format: date-time
download_status:
value:
type: boolean
timestamp:
type: string
format: date-time
favorite_status:
value:
type: boolean
timestamp:
type: string
format: date-time
example:
podcast_guid: 31740ac6-e39d-49cd-9179-634bcecf4143
sync_id: cff3ea32-4215-4f98-bc23-5358d1f35b55
episode_guid: https://example.com/podcast/episode-5-the-history-of-RSS
title: The history of RSS
publish_date: 2022-04-24T17:53:21.573Z
enclosure_url: https://example.com/podcast/episode-5-the-history-of-RSS.mp3
episode_url: https://example.com/podcast/episode-5-the-history-of-RSS
playback_position:
value: 0
timestamp: 2024-11-02T13:19
played_status:
value: true
timestamp: 2024-11-02T13:19
new_status:
value: false
timestamp: 2024-10-30T17:31
download_status:
value: false
timestamp: 2024-11-02T13:19
favorite_status:
value: false
timestamp: 2024-11-02T13:19
Episodes:
required:
- total
- page
- per_page
- episodes
xml:
name: episodes
type: object
properties:
total:
type: number
page:
type: number
per_page:
type: number
next:
type: string
format: url
previous:
type: string
format: url
episodes:
type: array
items:
$ref: '#/components/schemas/Episode'
example:
total: 2
page: 1
per_page: 5
episodes:
- podcast_guid: 31740ac6-e39d-49cd-9179-634bcecf4143
sync_id: cff3ea32-4215-4f98-bc23-5358d1f35b55
episode_guid: https://example.com/podcast/episode-5-the-history-of-RSS
title: The history of RSS
publish_date: 2022-04-24T17:53:21.573Z
enclosure_url: https://example.com/podcast/episode-5-the-history-of-RSS.mp3
episode_url: https://example.com/podcast/episode-5-the-history-of-RSS
playback_position:
value: 0
timestamp: 2024-11-02T13:19
played_status:
value: true
timestamp: 2024-11-02T13:19
new_status:
value: false
timestamp: 2024-10-30T17:31
download_status:
value: false
timestamp: 2024-11-02T13:19
favorite_status:
value: false
timestamp: 2024-11-02T13:19
- podcast_guid: 9d6786c9-ed48-470d-acbe-e593547f4b5b
sync_id: 5773f457-e71b-417d-8ea8-f07c38a03a3e
episode_guid: 01999e25-08cd-4f29-a61e-6ca459b40d27
title: Walk with the weatherman
publish_date: 2022-04-27T19:35:20.000Z
enclosure_url: https://op3.dev/e/https://podcasts.example2.net/audio/@digitalcitizen/49-walk-with-the-weatherman.mp3
episode_url: https://podcasts.example2.net/@digitalcitizen/episodes/49-walk-with-the-weatherman
playback_position:
value: 2100
timestamp: 2024-11-01T17:38
played_status:
value: false
timestamp: 2024-04-28T09:20
new_status:
value: false
timestamp: 2024-11-01T17:02
download_status:
value: true
timestamp: 2024-11-01T17:02
favorite_status:
value: false
timestamp: 2024-04-28T09:20
Deletion:
xml:
name: deletion
Expand Down Expand Up @@ -599,8 +808,8 @@ components:
implicit:
authorizationUrl: https://test.openpodcastapi.com/oauth/authorize
scopes:
write:subscriptions: modify subscription information for your account
read:subscriptions: read your subscription information
write:subscriptions: modify subscription information & related episodes for your account
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think reading & writing subscriptions & episodes can both be in the same scope. Would you agree @Sporiff @JonOfUs?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@keunes I think generally it's best practice with Oauth to keep read/write separate. The principle of least privilege dictates that you should give applications/people access only to what they absolutely need to get the job done. If you made an app that could read subscriptions to, for example, display your favorite podcasts on your website, you should not give that app access to modify those subscriptions.

read:subscriptions: read your subscription information & related episodes
api_key:
type: apiKey
name: api_key
Expand Down
5 changes: 5 additions & 0 deletions src/components/BadgeCore.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
import { Badge } from '@astrojs/starlight/components';
---

<Badge text="Core" variant="caution" size="small" />
5 changes: 5 additions & 0 deletions src/components/BadgeOptional.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
import { Badge } from '@astrojs/starlight/components';
---

<Badge text="Optional" variant="success" size="small" />
60 changes: 60 additions & 0 deletions src/content/docs/specs/episodes/add-update.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: Add or update episodes
description: Create new or update existing episodes for a user
sidebar:
order: 5
badge:
text: Core
variant: caution
---

import CoreAction from "@partials/_core-action.mdx";

<CoreAction />

```http title="Endpoint"
POST /v1/episodes
```

This endpoint enables clients to register new episodes for or change information of episodes relating to the authenticated user. It returns an array of `success` responses for newly added episodes, and an array of `failure` responses for episodes that couldn't be added.

This endpoint only accepts an array of objects, as it serves both for the creation and updating of a single and a batch of episodes.

## Related fields

See the [Overview](index) page for an overview of the fields used for this end-point.

## Request parameters

The client MUST provide a list of objects containing a set of parameters, which depend on the presence of a `sync_id`:
* if the `sync_id` of the episode is known:
* `podcast_guid`
* `sync_id` (of the episode)
* if there is no (known) `sync_id` (yet):
* all identifier fields except `sync_id` (`podcast_guid`, `episode_guid`, `title`, `publish_date`, `enclosure_url`, `episode_url`),
* `temporary_id` (optional),
* and always: any data data fields that were changed

## Client side behavior

:::note[Reminder: pull first, post later]
As discussed in the [Generic principles], clients SHOULD pull first and post later. In he process, clients are expected to do some [deduplication](identification-deduplication).
:::

Clients SHOULD adopt a **lazy synchronization** approach. This means:
* not syncing immediately when a new episode is found (e.g. after refreshing a feed)
* only syncing after an episode has been interacted with by the user or system (e.g. after initiating an automatic download)

Clients MAY leave the `sync_id` empty when registering new episodes, and leave the generation of the `sync_id` to the server.

Clients MAY provide a `temporary_id` with the request (e.g. the local database index) that the server will reflect in its response, so that the client can match the episodes from the server response more easily.

## Server side behavior

Servers SHOULD NOT rely on clients for episode discovery. If the server also has a 'client' component (a user interface to interact with subscriptions and episodes), it SHOULD independently ensure episode discovery (refresh feeds or rely on third party episode APIs). This is due to the lazy synchronization applied by clients - if servers do not fetch episodes themselves then the user might notice certain episodes are missing.

### `sync_id`

Servers MUST respond with a `sync_id` at all times. If none exists, they MUST be generated. Episode `sync_id`s are of type guid and could, for example, be the internal episode ID.

Servers MUST respond with the `temporary_id` for the episode if it was provided by the client. ??What did we say again about asynchronous processing? How long does this temporary ID need to be preserved by the server??
22 changes: 22 additions & 0 deletions src/content/docs/specs/episodes/get-all-from-subscription.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: Get all episodes of a podcast
description: Get all episodes for a subscription
sidebar:
order: 4
badge:
text: Core
variant: caution
---

import CoreAction from "@partials/_core-action.mdx";

<CoreAction />

```http title="Endpoint"
GET /v1/subscriptions/{guid}/episodes
```

TO DO. See [2023-05-30](https://pad.funkwhale.audio/oCfs5kJ6QTu02d_oVHW7DA#) meeting notes.


`GET/PUT /subscriptions/{guid}/episodes/{sync_id}` --> Really needed? `GET/PUT /episodes` would suffice probably.
Loading