diff --git a/docs/controllers/PodcastController.yaml b/docs/controllers/PodcastController.yaml new file mode 100644 index 00000000..a289eae3 --- /dev/null +++ b/docs/controllers/PodcastController.yaml @@ -0,0 +1,355 @@ +paths: + /api/podcasts: + post: + summary: Create a new podcast + operationId: createPodcast + tags: + - Podcasts + requestBody: + required: true + content: + application/json: + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/Podcast' + responses: + 200: + description: Successfully created a podcast + content: + application/json: + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/Podcast' + 400: + description: Bad request + 403: + description: Forbidden + 404: + description: Not found + + /api/podcasts/feed: + post: + summary: Get podcast feed + operationId: getPodcastFeed + tags: + - Podcasts + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + rssFeed: + type: string + description: The RSS feed URL of the podcast + responses: + 200: + description: Successfully retrieved podcast feed + content: + application/json: + schema: + type: object + properties: + podcast: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/Podcast' + 400: + description: Bad request + 403: + description: Forbidden + 404: + description: Not found + + /api/podcasts/opml: + post: + summary: Get feeds from OPML text + operationId: getFeedsFromOPMLText + tags: + - Podcasts + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + opmlText: + type: string + description: The OPML text containing podcast feeds + responses: + 200: + description: Successfully retrieved feeds from OPML text + content: + application/json: + schema: + type: array + items: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/Podcast' + 400: + description: Bad request + 403: + description: Forbidden + + /api/podcasts/{id}/checknew: + parameters: + - name: id + in: path + description: Podcast ID + required: true + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/podcastId' + + get: + summary: Check and download new episodes + operationId: checkNewEpisodes + tags: + - Podcasts + parameters: + - name: limit + in: query + description: Maximum number of episodes to download + required: false + schema: + type: integer + responses: + 200: + description: Successfully checked and downloaded new episodes + content: + application/json: + schema: + type: object + properties: + episodes: + type: array + items: + $ref: '../objects/entities/PodcastEpisode.yaml#/components/schemas/PodcastEpisode' + 403: + description: Forbidden + 404: + description: Not found + 500: + description: Server error + + /api/podcasts/{id}/clear-queue: + parameters: + - name: id + in: path + description: Podcast ID + required: true + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/podcastId' + + get: + summary: Clear episode download queue + operationId: clearEpisodeDownloadQueue + tags: + - Podcasts + responses: + 200: + description: Successfully cleared download queue + 403: + description: Forbidden + + /api/podcasts/{id}/downloads: + parameters: + - name: id + in: path + description: Podcast ID + required: true + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/podcastId' + + get: + summary: Get episode downloads + operationId: getEpisodeDownloads + tags: + - Podcasts + responses: + 200: + description: Successfully retrieved episode downloads + content: + application/json: + schema: + type: object + properties: + downloads: + type: array + items: + $ref: '../objects/entities/PodcastEpisode.yaml#/components/schemas/PodcastEpisode' + 404: + description: Not found + + /api/podcasts/{id}/search-episode: + parameters: + - name: id + in: path + description: Podcast ID + required: true + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/podcastId' + + get: + summary: Find episode by title + operationId: findEpisode + tags: + - Podcasts + parameters: + - name: title + in: query + description: Title of the episode to search for + required: true + schema: + type: string + responses: + 200: + description: Successfully found episodes + content: + application/json: + schema: + type: object + properties: + episodes: + type: array + items: + $ref: '../objects/entities/PodcastEpisode.yaml#/components/schemas/PodcastEpisode' + 404: + description: Not found + 500: + description: Server error + + /api/podcasts/{id}/download-episodes: + parameters: + - name: id + in: path + description: Podcast ID + required: true + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/podcastId' + + post: + summary: Download podcast episodes + operationId: downloadEpisodes + tags: + - Podcasts + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: string + responses: + 200: + description: Successfully started episode download + 400: + description: Bad request + 403: + description: Forbidden + + /api/podcasts/{id}/match-episodes: + parameters: + - name: id + in: path + description: Podcast ID + required: true + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/podcastId' + + post: + summary: Quick match podcast episodes + operationId: quickMatchEpisodes + tags: + - Podcasts + parameters: + - name: override + in: query + description: Override existing details if set to 1 + required: false + schema: + type: string + responses: + 200: + description: Successfully matched episodes + content: + application/json: + schema: + type: object + properties: + numEpisodesUpdated: + type: integer + 403: + description: Forbidden + + /api/podcasts/{id}/episode/{episodeId}: + parameters: + - name: id + in: path + description: Podcast ID + required: true + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/podcastId' + - name: episodeId + in: path + description: Episode ID + required: true + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/podcastId' + + patch: + summary: Update a podcast episode + operationId: updateEpisode + tags: + - Podcasts + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + 200: + description: Successfully updated episode + content: + application/json: + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/Podcast' + 404: + description: Not found + + get: + summary: Get a specific podcast episode + operationId: getEpisode + tags: + - Podcasts + responses: + 200: + description: Successfully retrieved episode + content: + application/json: + schema: + $ref: '../objects/entities/PodcastEpisode.yaml#/components/schemas/PodcastEpisode' + 404: + description: Not found + + delete: + summary: Remove a podcast episode + operationId: removeEpisode + tags: + - Podcasts + parameters: + - name: hard + in: query + description: Hard delete the episode if set to 1 + required: false + schema: + type: string + responses: + 200: + description: Successfully removed episode + content: + application/json: + schema: + $ref: '../objects/mediaTypes/Podcast.yaml#/components/schemas/Podcast' + 404: + description: Not found + 500: + description: Server error diff --git a/docs/objects/entities/PodcastEpisode.yaml b/docs/objects/entities/PodcastEpisode.yaml new file mode 100644 index 00000000..f10c25ec --- /dev/null +++ b/docs/objects/entities/PodcastEpisode.yaml @@ -0,0 +1,74 @@ +components: + schemas: + PodcastEpisode: + type: object + description: A single episode of a podcast. + properties: + libraryItemId: + $ref: '../LibraryItem.yaml#/components/schemas/libraryItemId' + podcastId: + $ref: '../mediaTypes/Podcast.yaml#/components/schemas/podcastId' + id: + $ref: '../mediaTypes/Podcast.yaml#/components/schemas/podcastId' + oldEpisodeId: + $ref: '../mediaTypes/Podcast.yaml#/components/schemas/oldPodcastId' + index: + type: integer + description: The index of the episode within the podcast. + nullable: true + season: + type: string + description: The season number of the episode. + nullable: true + episode: + type: string + description: The episode number within the season. + nullable: true + episodeType: + type: string + description: The type of episode (e.g., full, trailer). + nullable: true + title: + type: string + description: The title of the episode. + nullable: true + subtitle: + type: string + description: The subtitle of the episode. + nullable: true + description: + type: string + description: The description of the episode. + nullable: true + enclosure: + type: object + description: The enclosure object containing additional episode data. + nullable: true + additionalProperties: true + guid: + type: string + description: The globally unique identifier for the episode. + nullable: true + pubDate: + type: string + description: The publication date of the episode. + nullable: true + chapters: + type: array + description: The chapters within the episode. + items: + type: object + audioFile: + $ref: '../files/AudioFile.yaml#/components/schemas/audioFile' + publishedAt: + $ref: '../../schemas.yaml#/components/schemas/createdAt' + addedAt: + $ref: '../../schemas.yaml#/components/schemas/addedAt' + updatedAt: + $ref: '../../schemas.yaml#/components/schemas/updatedAt' + audioTrack: + $ref: '../files/AudioTrack.yaml#/components/schemas/AudioTrack' + duration: + $ref: '../../schemas.yaml#/components/schemas/durationSec' + size: + $ref: '../../schemas.yaml#/components/schemas/size' diff --git a/docs/objects/files/AudioTrack.yaml b/docs/objects/files/AudioTrack.yaml new file mode 100644 index 00000000..d31c5cef --- /dev/null +++ b/docs/objects/files/AudioTrack.yaml @@ -0,0 +1,45 @@ +components: + schemas: + AudioTrack: + type: object + description: Represents an audio track with various properties. + properties: + index: + type: integer + nullable: true + description: The index of the audio track. + example: null + startOffset: + type: number + format: float + nullable: true + description: The start offset of the audio track in seconds. + example: null + duration: + type: number + format: float + nullable: true + description: The duration of the audio track in seconds. + example: null + title: + type: string + nullable: true + description: The title of the audio track. + example: null + contentUrl: + type: string + nullable: true + description: The URL where the audio track content is located. + example: '`/api/items/${itemId}/file/${audioFile.ino}`' + mimeType: + type: string + nullable: true + description: The MIME type of the audio track. + example: null + codec: + type: string + nullable: true + description: The codec used for the audio track. + example: aac + metadata: + $ref: '../metadata/FileMetadata.yaml#/components/schemas/fileMetadata' diff --git a/docs/objects/mediaTypes/Podcast.yaml b/docs/objects/mediaTypes/Podcast.yaml new file mode 100644 index 00000000..8cc351bf --- /dev/null +++ b/docs/objects/mediaTypes/Podcast.yaml @@ -0,0 +1,74 @@ +components: + schemas: + podcastId: + type: string + description: The ID of podcasts and podcast episodes after 2.3.0. + format: uuid + example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b + oldPodcastId: + description: The ID of podcasts on server version 2.2.23 and before. + type: string + nullable: true + format: 'pod_[a-z0-9]{18}' + example: pod_o78uaoeuh78h6aoeif + + Podcast: + type: object + description: A podcast containing multiple episodes. + properties: + id: + $ref: '#/components/schemas/podcastId' + libraryItemId: + $ref: '../LibraryItem.yaml#/components/schemas/libraryItemId' + metadata: + $ref: '../metadata/PodcastMetadata.yaml#/components/schemas/PodcastMetadata' + coverPath: + type: string + description: The file path to the podcast's cover image. + nullable: true + tags: + type: array + description: The tags associated with the podcast. + items: + type: string + episodes: + type: array + description: The episodes of the podcast. + items: + $ref: '../entities/PodcastEpisode.yaml#/components/schemas/PodcastEpisode' + autoDownloadEpisodes: + type: boolean + description: Whether episodes are automatically downloaded. + autoDownloadSchedule: + type: string + description: The schedule for automatic episode downloads, in cron format. + nullable: true + lastEpisodeCheck: + type: integer + description: The timestamp of the last episode check. + maxEpisodesToKeep: + type: integer + description: The maximum number of episodes to keep. + maxNewEpisodesToDownload: + type: integer + description: The maximum number of new episodes to download when automatically downloading epsiodes. + lastCoverSearch: + type: integer + description: The timestamp of the last cover search. + nullable: true + lastCoverSearchQuery: + type: string + description: The query used for the last cover search. + nullable: true + size: + type: integer + description: The total size of all episodes in bytes. + duration: + type: integer + description: The total duration of all episodes in seconds. + numTracks: + type: integer + description: The number of tracks (episodes) in the podcast. + latestEpisodePublished: + type: integer + description: The timestamp of the most recently published episode. diff --git a/docs/objects/metadata/PodcastMetadata.yaml b/docs/objects/metadata/PodcastMetadata.yaml new file mode 100644 index 00000000..a565d697 --- /dev/null +++ b/docs/objects/metadata/PodcastMetadata.yaml @@ -0,0 +1,59 @@ +components: + schemas: + PodcastMetadata: + type: object + description: Metadata for a podcast. + properties: + title: + type: string + description: The title of the podcast. + nullable: true + author: + type: string + description: The author of the podcast. + nullable: true + description: + type: string + description: The description of the podcast. + nullable: true + releaseDate: + type: string + format: date-time + description: The release date of the podcast. + nullable: true + genres: + type: array + description: The genres of the podcast. + items: + type: string + feedUrl: + type: string + description: The URL of the podcast feed. + nullable: true + imageUrl: + type: string + description: The URL of the podcast's image. + nullable: true + itunesPageUrl: + type: string + description: The URL of the podcast's iTunes page. + nullable: true + itunesId: + type: string + description: The iTunes ID of the podcast. + nullable: true + itunesArtistId: + type: string + description: The iTunes artist ID of the podcast. + nullable: true + explicit: + type: boolean + description: Whether the podcast contains explicit content. + language: + type: string + description: The language of the podcast. + nullable: true + type: + type: string + description: The type of podcast (e.g., episodic, serial). + nullable: true diff --git a/docs/openapi.json b/docs/openapi.json index 33669d58..38e37ee0 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -36,6 +36,10 @@ { "name": "Notification", "description": "Notifications endpoints" + }, + { + "name": "Podcasts", + "description": "Podcast endpoints" } ], "paths": { @@ -1494,6 +1498,538 @@ } } }, + "/api/podcasts": { + "post": { + "summary": "Create a new podcast", + "operationId": "createPodcast", + "tags": [ + "Podcasts" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Podcast" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully created a podcast", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Podcast" + } + } + } + }, + "400": { + "description": "Bad request" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/podcasts/feed": { + "post": { + "summary": "Get podcast feed", + "operationId": "getPodcastFeed", + "tags": [ + "Podcasts" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "rssFeed": { + "type": "string", + "description": "The RSS feed URL of the podcast" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully retrieved podcast feed", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "podcast": { + "$ref": "#/components/schemas/Podcast" + } + } + } + } + } + }, + "400": { + "description": "Bad request" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/podcasts/opml": { + "post": { + "summary": "Get feeds from OPML text", + "operationId": "getFeedsFromOPMLText", + "tags": [ + "Podcasts" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "opmlText": { + "type": "string", + "description": "The OPML text containing podcast feeds" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully retrieved feeds from OPML text", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Podcast" + } + } + } + } + }, + "400": { + "description": "Bad request" + }, + "403": { + "description": "Forbidden" + } + } + } + }, + "/api/podcasts/{id}/checknew": { + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Podcast ID", + "required": true, + "schema": { + "$ref": "#/components/schemas/podcastId" + } + } + ], + "get": { + "summary": "Check and download new episodes", + "operationId": "checkNewEpisodes", + "tags": [ + "Podcasts" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "Maximum number of episodes to download", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successfully checked and downloaded new episodes", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "episodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PodcastEpisode" + } + } + } + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not found" + }, + "500": { + "description": "Server error" + } + } + } + }, + "/api/podcasts/{id}/clear-queue": { + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Podcast ID", + "required": true, + "schema": { + "$ref": "#/components/schemas/podcastId" + } + } + ], + "get": { + "summary": "Clear episode download queue", + "operationId": "clearEpisodeDownloadQueue", + "tags": [ + "Podcasts" + ], + "responses": { + "200": { + "description": "Successfully cleared download queue" + }, + "403": { + "description": "Forbidden" + } + } + } + }, + "/api/podcasts/{id}/downloads": { + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Podcast ID", + "required": true, + "schema": { + "$ref": "#/components/schemas/podcastId" + } + } + ], + "get": { + "summary": "Get episode downloads", + "operationId": "getEpisodeDownloads", + "tags": [ + "Podcasts" + ], + "responses": { + "200": { + "description": "Successfully retrieved episode downloads", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "downloads": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PodcastEpisode" + } + } + } + } + } + } + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/podcasts/{id}/search-episode": { + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Podcast ID", + "required": true, + "schema": { + "$ref": "#/components/schemas/podcastId" + } + } + ], + "get": { + "summary": "Find episode by title", + "operationId": "findEpisode", + "tags": [ + "Podcasts" + ], + "parameters": [ + { + "name": "title", + "in": "query", + "description": "Title of the episode to search for", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully found episodes", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "episodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PodcastEpisode" + } + } + } + } + } + } + }, + "404": { + "description": "Not found" + }, + "500": { + "description": "Server error" + } + } + } + }, + "/api/podcasts/{id}/download-episodes": { + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Podcast ID", + "required": true, + "schema": { + "$ref": "#/components/schemas/podcastId" + } + } + ], + "post": { + "summary": "Download podcast episodes", + "operationId": "downloadEpisodes", + "tags": [ + "Podcasts" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully started episode download" + }, + "400": { + "description": "Bad request" + }, + "403": { + "description": "Forbidden" + } + } + } + }, + "/api/podcasts/{id}/match-episodes": { + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Podcast ID", + "required": true, + "schema": { + "$ref": "#/components/schemas/podcastId" + } + } + ], + "post": { + "summary": "Quick match podcast episodes", + "operationId": "quickMatchEpisodes", + "tags": [ + "Podcasts" + ], + "parameters": [ + { + "name": "override", + "in": "query", + "description": "Override existing details if set to 1", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully matched episodes", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "numEpisodesUpdated": { + "type": "integer" + } + } + } + } + } + }, + "403": { + "description": "Forbidden" + } + } + } + }, + "/api/podcasts/{id}/episode/{episodeId}": { + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Podcast ID", + "required": true, + "schema": { + "$ref": "#/components/schemas/podcastId" + } + }, + { + "name": "episodeId", + "in": "path", + "description": "Episode ID", + "required": true, + "schema": { + "$ref": "#/components/schemas/podcastId" + } + } + ], + "patch": { + "summary": "Update a podcast episode", + "operationId": "updateEpisode", + "tags": [ + "Podcasts" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully updated episode", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Podcast" + } + } + } + }, + "404": { + "description": "Not found" + } + } + }, + "get": { + "summary": "Get a specific podcast episode", + "operationId": "getEpisode", + "tags": [ + "Podcasts" + ], + "responses": { + "200": { + "description": "Successfully retrieved episode", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PodcastEpisode" + } + } + } + }, + "404": { + "description": "Not found" + } + } + }, + "delete": { + "summary": "Remove a podcast episode", + "operationId": "removeEpisode", + "tags": [ + "Podcasts" + ], + "parameters": [ + { + "name": "hard", + "in": "query", + "description": "Hard delete the episode if set to 1", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully removed episode", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Podcast" + } + } + } + }, + "404": { + "description": "Not found" + }, + "500": { + "description": "Server error" + } + } + } + }, "/api/series/{id}": { "parameters": [ { @@ -2759,6 +3295,647 @@ "description": "The time (in ms) between notification pushes." } } + }, + "podcastId": { + "type": "string", + "description": "The ID of podcasts and podcast episodes after 2.3.0.", + "format": "uuid", + "example": "e4bb1afb-4a4f-4dd6-8be0-e615d233185b" + }, + "PodcastMetadata": { + "type": "object", + "description": "Metadata for a podcast.", + "properties": { + "title": { + "type": "string", + "description": "The title of the podcast.", + "nullable": true + }, + "author": { + "type": "string", + "description": "The author of the podcast.", + "nullable": true + }, + "description": { + "type": "string", + "description": "The description of the podcast.", + "nullable": true + }, + "releaseDate": { + "type": "string", + "format": "date-time", + "description": "The release date of the podcast.", + "nullable": true + }, + "genres": { + "type": "array", + "description": "The genres of the podcast.", + "items": { + "type": "string" + } + }, + "feedUrl": { + "type": "string", + "description": "The URL of the podcast feed.", + "nullable": true + }, + "imageUrl": { + "type": "string", + "description": "The URL of the podcast's image.", + "nullable": true + }, + "itunesPageUrl": { + "type": "string", + "description": "The URL of the podcast's iTunes page.", + "nullable": true + }, + "itunesId": { + "type": "string", + "description": "The iTunes ID of the podcast.", + "nullable": true + }, + "itunesArtistId": { + "type": "string", + "description": "The iTunes artist ID of the podcast.", + "nullable": true + }, + "explicit": { + "type": "boolean", + "description": "Whether the podcast contains explicit content." + }, + "language": { + "type": "string", + "description": "The language of the podcast.", + "nullable": true + }, + "type": { + "type": "string", + "description": "The type of podcast (e.g., episodic, serial).", + "nullable": true + } + } + }, + "oldPodcastId": { + "description": "The ID of podcasts on server version 2.2.23 and before.", + "type": "string", + "nullable": true, + "format": "pod_[a-z0-9]{18}", + "example": "pod_o78uaoeuh78h6aoeif" + }, + "fileMetadata": { + "type": "object", + "description": "The metadata for a file, including the path, size, and unix timestamps of the file.", + "nullable": true, + "properties": { + "filename": { + "description": "The filename of the file.", + "type": "string", + "example": "Wizards First Rule 01.mp3" + }, + "ext": { + "description": "The file extension of the file.", + "type": "string", + "example": ".mp3" + }, + "path": { + "description": "The absolute path on the server of the file.", + "type": "string", + "example": "/audiobooks/Terry Goodkind/Sword of Truth/Wizards First Rule/Terry Goodkind - SOT Bk01 - Wizards First Rule 01.mp3" + }, + "relPath": { + "description": "The path of the file, relative to the book's or podcast's folder.", + "type": "string", + "example": "Wizards First Rule 01.mp3" + }, + "size": { + "$ref": "#/components/schemas/size" + }, + "mtimeMs": { + "description": "The time (in ms since POSIX epoch) when the file was last modified on disk.", + "type": "integer", + "example": 1632223180278 + }, + "ctimeMs": { + "description": "The time (in ms since POSIX epoch) when the file status was changed on disk.", + "type": "integer", + "example": 1645978261001 + }, + "birthtimeMs": { + "description": "The time (in ms since POSIX epoch) when the file was created on disk. Will be 0 if unknown.", + "type": "integer", + "example": 0 + } + } + }, + "bookChapter": { + "type": "object", + "description": "A book chapter. Includes the title and timestamps.", + "properties": { + "id": { + "description": "The ID of the book chapter.", + "type": "integer", + "example": 0 + }, + "start": { + "description": "When in the book (in seconds) the chapter starts.", + "type": "integer", + "example": 0 + }, + "end": { + "description": "When in the book (in seconds) the chapter ends.", + "type": "number", + "example": 6004.6675 + }, + "title": { + "description": "The title of the chapter.", + "type": "string", + "example": "Wizards First Rule 01 Chapter 1" + } + } + }, + "audioMetaTags": { + "description": "ID3 metadata tags pulled from the audio file on import. Only non-null tags will be returned in requests.", + "type": "object", + "properties": { + "tagAlbum": { + "type": "string", + "nullable": true, + "example": "SOT Bk01" + }, + "tagArtist": { + "type": "string", + "nullable": true, + "example": "Terry Goodkind" + }, + "tagGenre": { + "type": "string", + "nullable": true, + "example": "Audiobook Fantasy" + }, + "tagTitle": { + "type": "string", + "nullable": true, + "example": "Wizards First Rule 01" + }, + "tagSeries": { + "type": "string", + "nullable": true + }, + "tagSeriesPart": { + "type": "string", + "nullable": true + }, + "tagTrack": { + "type": "string", + "nullable": true, + "example": "01/20" + }, + "tagDisc": { + "type": "string", + "nullable": true + }, + "tagSubtitle": { + "type": "string", + "nullable": true + }, + "tagAlbumArtist": { + "type": "string", + "nullable": true, + "example": "Terry Goodkind" + }, + "tagDate": { + "type": "string", + "nullable": true + }, + "tagComposer": { + "type": "string", + "nullable": true, + "example": "Terry Goodkind" + }, + "tagPublisher": { + "type": "string", + "nullable": true + }, + "tagComment": { + "type": "string", + "nullable": true + }, + "tagDescription": { + "type": "string", + "nullable": true + }, + "tagEncoder": { + "type": "string", + "nullable": true + }, + "tagEncodedBy": { + "type": "string", + "nullable": true + }, + "tagIsbn": { + "type": "string", + "nullable": true + }, + "tagLanguage": { + "type": "string", + "nullable": true + }, + "tagASIN": { + "type": "string", + "nullable": true + }, + "tagOverdriveMediaMarker": { + "type": "string", + "nullable": true + }, + "tagOriginalYear": { + "type": "string", + "nullable": true + }, + "tagReleaseCountry": { + "type": "string", + "nullable": true + }, + "tagReleaseType": { + "type": "string", + "nullable": true + }, + "tagReleaseStatus": { + "type": "string", + "nullable": true + }, + "tagISRC": { + "type": "string", + "nullable": true + }, + "tagMusicBrainzTrackId": { + "type": "string", + "nullable": true + }, + "tagMusicBrainzAlbumId": { + "type": "string", + "nullable": true + }, + "tagMusicBrainzAlbumArtistId": { + "type": "string", + "nullable": true + }, + "tagMusicBrainzArtistId": { + "type": "string", + "nullable": true + } + } + }, + "audioFile": { + "type": "object", + "description": "An audio file for a book. Includes audio metadata and track numbers.", + "properties": { + "index": { + "description": "The index of the audio file.", + "type": "integer", + "example": 1 + }, + "ino": { + "$ref": "#/components/schemas/inode" + }, + "metadata": { + "$ref": "#/components/schemas/fileMetadata" + }, + "addedAt": { + "$ref": "#/components/schemas/addedAt" + }, + "updatedAt": { + "$ref": "#/components/schemas/updatedAt" + }, + "trackNumFromMeta": { + "description": "The track number of the audio file as pulled from the file's metadata. Will be null if unknown.", + "type": "integer", + "nullable": true, + "example": 1 + }, + "discNumFromMeta": { + "description": "The disc number of the audio file as pulled from the file's metadata. Will be null if unknown.", + "type": "string", + "nullable": true + }, + "trackNumFromFilename": { + "description": "The track number of the audio file as determined from the file's name. Will be null if unknown.", + "type": "integer", + "nullable": true, + "example": 1 + }, + "discNumFromFilename": { + "description": "The disc number of the audio file as determined from the file's name. Will be null if unknown.", + "type": "string", + "nullable": true + }, + "manuallyVerified": { + "description": "Whether the audio file has been manually verified by a user.", + "type": "boolean" + }, + "invalid": { + "description": "Whether the audio file is missing from the server.", + "type": "boolean" + }, + "exclude": { + "description": "Whether the audio file has been marked for exclusion.", + "type": "boolean" + }, + "error": { + "description": "Any error with the audio file. Will be null if there is none.", + "type": "string", + "nullable": true + }, + "format": { + "description": "The format of the audio file.", + "type": "string", + "example": "MP2/3 (MPEG audio layer 2/3)" + }, + "duration": { + "$ref": "#/components/schemas/durationSec" + }, + "bitRate": { + "description": "The bit rate (in bit/s) of the audio file.", + "type": "integer", + "example": 64000 + }, + "language": { + "description": "The language of the audio file.", + "type": "string", + "nullable": true + }, + "codec": { + "description": "The codec of the audio file.", + "type": "string", + "example": "mp3" + }, + "timeBase": { + "description": "The time base of the audio file.", + "type": "string", + "example": "1/14112000" + }, + "channels": { + "description": "The number of channels the audio file has.", + "type": "integer", + "example": 2 + }, + "channelLayout": { + "description": "The layout of the audio file's channels.", + "type": "string", + "example": "stereo" + }, + "chapters": { + "description": "If the audio file is part of an audiobook, the chapters the file contains.", + "type": "array", + "items": { + "$ref": "#/components/schemas/bookChapter" + } + }, + "embeddedCoverArt": { + "description": "The type of embedded cover art in the audio file. Will be null if none exists.", + "type": "string", + "nullable": true + }, + "metaTags": { + "$ref": "#/components/schemas/audioMetaTags" + }, + "mimeType": { + "description": "The MIME type of the audio file.", + "type": "string", + "example": "audio/mpeg" + } + } + }, + "AudioTrack": { + "type": "object", + "description": "Represents an audio track with various properties.", + "properties": { + "index": { + "type": "integer", + "nullable": true, + "description": "The index of the audio track.", + "example": null + }, + "startOffset": { + "type": "number", + "format": "float", + "nullable": true, + "description": "The start offset of the audio track in seconds.", + "example": null + }, + "duration": { + "type": "number", + "format": "float", + "nullable": true, + "description": "The duration of the audio track in seconds.", + "example": null + }, + "title": { + "type": "string", + "nullable": true, + "description": "The title of the audio track.", + "example": null + }, + "contentUrl": { + "type": "string", + "nullable": true, + "description": "The URL where the audio track content is located.", + "example": "`/api/items/${itemId}/file/${audioFile.ino}`" + }, + "mimeType": { + "type": "string", + "nullable": true, + "description": "The MIME type of the audio track.", + "example": null + }, + "codec": { + "type": "string", + "nullable": true, + "description": "The codec used for the audio track.", + "example": "aac" + }, + "metadata": { + "$ref": "#/components/schemas/fileMetadata" + } + } + }, + "PodcastEpisode": { + "type": "object", + "description": "A single episode of a podcast.", + "properties": { + "libraryItemId": { + "$ref": "#/components/schemas/libraryItemId" + }, + "podcastId": { + "$ref": "#/components/schemas/podcastId" + }, + "id": { + "$ref": "#/components/schemas/podcastId" + }, + "oldEpisodeId": { + "$ref": "#/components/schemas/oldPodcastId" + }, + "index": { + "type": "integer", + "description": "The index of the episode within the podcast.", + "nullable": true + }, + "season": { + "type": "string", + "description": "The season number of the episode.", + "nullable": true + }, + "episode": { + "type": "string", + "description": "The episode number within the season.", + "nullable": true + }, + "episodeType": { + "type": "string", + "description": "The type of episode (e.g., full, trailer).", + "nullable": true + }, + "title": { + "type": "string", + "description": "The title of the episode.", + "nullable": true + }, + "subtitle": { + "type": "string", + "description": "The subtitle of the episode.", + "nullable": true + }, + "description": { + "type": "string", + "description": "The description of the episode.", + "nullable": true + }, + "enclosure": { + "type": "object", + "description": "The enclosure object containing additional episode data.", + "nullable": true, + "additionalProperties": true + }, + "guid": { + "type": "string", + "description": "The globally unique identifier for the episode.", + "nullable": true + }, + "pubDate": { + "type": "string", + "description": "The publication date of the episode.", + "nullable": true + }, + "chapters": { + "type": "array", + "description": "The chapters within the episode.", + "items": { + "type": "object" + } + }, + "audioFile": { + "$ref": "#/components/schemas/audioFile" + }, + "publishedAt": { + "$ref": "#/components/schemas/createdAt" + }, + "addedAt": { + "$ref": "#/components/schemas/addedAt" + }, + "updatedAt": { + "$ref": "#/components/schemas/updatedAt" + }, + "audioTrack": { + "$ref": "#/components/schemas/AudioTrack" + }, + "duration": { + "$ref": "#/components/schemas/durationSec" + }, + "size": { + "$ref": "#/components/schemas/size" + } + } + }, + "Podcast": { + "type": "object", + "description": "A podcast containing multiple episodes.", + "properties": { + "id": { + "$ref": "#/components/schemas/podcastId" + }, + "libraryItemId": { + "$ref": "#/components/schemas/libraryItemId" + }, + "metadata": { + "$ref": "#/components/schemas/PodcastMetadata" + }, + "coverPath": { + "type": "string", + "description": "The file path to the podcast's cover image.", + "nullable": true + }, + "tags": { + "type": "array", + "description": "The tags associated with the podcast.", + "items": { + "type": "string" + } + }, + "episodes": { + "type": "array", + "description": "The episodes of the podcast.", + "items": { + "$ref": "#/components/schemas/PodcastEpisode" + } + }, + "autoDownloadEpisodes": { + "type": "boolean", + "description": "Whether episodes are automatically downloaded." + }, + "autoDownloadSchedule": { + "type": "string", + "description": "The schedule for automatic episode downloads, in cron format.", + "nullable": true + }, + "lastEpisodeCheck": { + "type": "integer", + "description": "The timestamp of the last episode check." + }, + "maxEpisodesToKeep": { + "type": "integer", + "description": "The maximum number of episodes to keep." + }, + "maxNewEpisodesToDownload": { + "type": "integer", + "description": "The maximum number of new episodes to download when automatically downloading epsiodes." + }, + "lastCoverSearch": { + "type": "integer", + "description": "The timestamp of the last cover search.", + "nullable": true + }, + "lastCoverSearchQuery": { + "type": "string", + "description": "The query used for the last cover search.", + "nullable": true + }, + "size": { + "type": "integer", + "description": "The total size of all episodes in bytes." + }, + "duration": { + "type": "integer", + "description": "The total duration of all episodes in seconds." + }, + "numTracks": { + "type": "integer", + "description": "The number of tracks (episodes) in the podcast." + }, + "latestEpisodePublished": { + "type": "integer", + "description": "The timestamp of the most recently published episode." + } + } } }, "responses": { diff --git a/docs/root.yaml b/docs/root.yaml index 4ac22abc..f4ceee50 100644 --- a/docs/root.yaml +++ b/docs/root.yaml @@ -53,6 +53,26 @@ paths: $ref: './controllers/NotificationController.yaml#/paths/~1api~1notifications~1{id}' /api/notifications/{id}/test: $ref: './controllers/NotificationController.yaml#/paths/~1api~1notifications~1{id}~1test' + /api/podcasts: + $ref: './controllers/PodcastController.yaml#/paths/~1api~1podcasts' + /api/podcasts/feed: + $ref: './controllers/PodcastController.yaml#/paths/~1api~1podcasts~1feed' + /api/podcasts/opml: + $ref: './controllers/PodcastController.yaml#/paths/~1api~1podcasts~1opml' + /api/podcasts/{id}/checknew: + $ref: './controllers/PodcastController.yaml#/paths/~1api~1podcasts~1{id}~1checknew' + /api/podcasts/{id}/clear-queue: + $ref: './controllers/PodcastController.yaml#/paths/~1api~1podcasts~1{id}~1clear-queue' + /api/podcasts/{id}/downloads: + $ref: './controllers/PodcastController.yaml#/paths/~1api~1podcasts~1{id}~1downloads' + /api/podcasts/{id}/search-episode: + $ref: './controllers/PodcastController.yaml#/paths/~1api~1podcasts~1{id}~1search-episode' + /api/podcasts/{id}/download-episodes: + $ref: './controllers/PodcastController.yaml#/paths/~1api~1podcasts~1{id}~1download-episodes' + /api/podcasts/{id}/match-episodes: + $ref: './controllers/PodcastController.yaml#/paths/~1api~1podcasts~1{id}~1match-episodes' + /api/podcasts/{id}/episode/{episodeId}: + $ref: './controllers/PodcastController.yaml#/paths/~1api~1podcasts~1{id}~1episode~1{episodeId}' /api/series/{id}: $ref: './controllers/SeriesController.yaml#/paths/~1api~1series~1{id}' tags: @@ -66,3 +86,5 @@ tags: description: Email endpoints - name: Notification description: Notifications endpoints + - name: Podcasts + description: Podcast endpoints diff --git a/docs/schemas.yaml b/docs/schemas.yaml index 07289561..e4e05e80 100644 --- a/docs/schemas.yaml +++ b/docs/schemas.yaml @@ -20,6 +20,10 @@ components: description: The total length (in seconds) of the item or file. type: number example: 33854.905 + duration: + description: The total length of the item or file. + type: string + example: '01:23:45' tags: description: Tags applied to items. type: array