diff --git a/api/Playlists.php b/api/Playlists.php index a1005da2..c6f467b0 100644 --- a/api/Playlists.php +++ b/api/Playlists.php @@ -3,7 +3,7 @@ * Zookeeper Online * * @author Jim Mason - * @copyright Copyright (C) 1997-2023 Jim Mason + * @copyright Copyright (C) 1997-2024 Jim Mason * @link https://zookeeper.ibinx.com/ * @license GPL-3.0 * @@ -121,6 +121,60 @@ private static function paginate(RequestInterface $request, $type, $key, &$offse return [$size, $result]; } + private static function fetchEvents($playlist, $aflags) { + $relations = new ResourceCollection(); + + Engine::api(IPlaylist::class)->getTracksWithObserver($playlist, + (new PlaylistObserver())->onComment(function($entry) use($relations) { + $e = new JsonResource("event", $entry->getId()); + $a = $e->attributes(); + $a->set("type", "comment"); + $a->set("comment", $entry->getComment()); + $a->set("created", $entry->getCreatedTime()); + $relations->set($e); + })->onLogEvent(function($entry) use($relations) { + $e = new JsonResource("event", $entry->getId()); + $a = $e->attributes(); + $a->set("type", "logEvent"); + $a->set("event", $entry->getLogEventType()); + $a->set("code", $entry->getLogEventCode()); + $a->set("created", $entry->getCreatedTime()); + $relations->set($e); + })->onSetSeparator(function($entry) use($relations) { + $e = new JsonResource("event", $entry->getId()); + $a = $e->attributes(); + $a->set("type", "break"); + $a->set("created", $entry->getCreatedTime()); + $relations->set($e); + })->onSpin(function($entry) use($relations, $aflags) { + $e = new JsonResource("event", $entry->getId()); + $a = $e->attributes(); + $a->set("type", "spin"); + $attrs = $entry->asArray(); + unset($attrs["tag"]); + unset($attrs["id"]); + $a->merge($attrs); + $a->set("created", $entry->getCreatedTime()); + + $tag = $entry->getTag(); + if($tag) { + $a->set("artist", PlaylistEntry::swapNames($entry->getArtist())); + if($aflags && sizeof($albums = Engine::api(ILibrary::class)->search(ILibrary::ALBUM_KEY, 0, 1, $tag))) + $res = Albums::fromArray($albums, $aflags)[0]; + else + $res = new JsonResource("album", $tag); + + $relation = new Relationship("album", $res); + $relation->links()->set(new Link("related", Engine::getBaseUrl()."album/$tag")); + $e->relationships()->set($relation); + } + + $relations->set($e); + })); + + return $relations; + } + public static function fromRecord($rec, $flags) { $id = $rec["list"] ?? $rec["id"]; $res = new JsonResource("show", $id); @@ -149,6 +203,17 @@ public static function fromRecord($rec, $flags) { } if($flags & self::LINKS_EVENTS) { + if(Engine::getApiVer() >= 2) { + $aflags = $flags & self::LINKS_ALBUMS_DETAILS ? + Albums::LINKS_ALL : Albums::LINKS_NONE; + + $relations = self::fetchEvents($id, $aflags); + $relation = new Relationship("events", $relations); + $relation->links()->set(new Link("related", Engine::getBaseUrl()."playlist/$id/events")); + $res->relationships()->set($relation); + return $res; + } + $relations = new ResourceCollection(); $events = []; @@ -223,9 +288,11 @@ public function fetchResource(RequestInterface $request): ResponseInterface { $row["list"] = $key; $flags = self::LINKS_NONE; - if($request->requestsField("show", "events")) + $apiver = Engine::getApiVer(); + if($apiver >= 2 || $request->requestsField("show", "events")) $flags |= self::LINKS_EVENTS | self::LINKS_ALBUMS; - if($request->requestsInclude("albums")) + if($request->requestsInclude("events.album") || + $apiver < 2 && $request->requestsInclude("albums")) $flags |= self::LINKS_ALBUMS_DETAILS; if($request->requestsInclude("origin")) $flags |= self::LINKS_ORIGIN; @@ -234,21 +301,25 @@ public function fetchResource(RequestInterface $request): ResponseInterface { $document = new Document($resource); $response = new DocumentResponse($document); - $response->headers()->set('Content-Type', ApiServer::CONTENT_TYPE); + if(Engine::getApiVer() < 2) + $response->headers()->set('Content-Type', ApiServer::CONTENT_TYPE); return $response; } public function fetchResources(RequestInterface $request): ResponseInterface { $flags = self::LINKS_NONE; - if($request->requestsField("show", "events")) + $apiver = Engine::getApiVer(); + if($apiver >= 2 || $request->requestsField("show", "events")) $flags |= self::LINKS_EVENTS | self::LINKS_ALBUMS; - if($request->requestsInclude("albums")) + if($request->requestsInclude("events.album") || + $apiver < 2 && $request->requestsInclude("albums")) $flags |= self::LINKS_ALBUMS_DETAILS; if($request->requestsInclude("origin")) $flags |= self::LINKS_ORIGIN; $response = $this->paginateOffset($request, self::$paginateOps, $flags); - $response->headers()->set('Content-Type', ApiServer::CONTENT_TYPE); + if(Engine::getApiVer() < 2) + $response->headers()->set('Content-Type', ApiServer::CONTENT_TYPE); return $response; } @@ -272,6 +343,9 @@ public function fetchRelationship(RequestInterface $request): ResponseInterface switch($request->relationship()) { case "albums": + if(Engine::getApiVer() >= 2) + throw new NotAllowedException('You are not allowed to fetch the relationship ' . $request->relationship()); + $api->getTracksWithObserver($id, (new PlaylistObserver())->onSpin(function($entry) use($relations, $flags) { $tag = $entry->getTag(); @@ -288,51 +362,8 @@ public function fetchRelationship(RequestInterface $request): ResponseInterface case "events": if(!$request->requestsInclude("album")) $flags = Albums::LINKS_NONE; - $api->getTracksWithObserver($id, - (new PlaylistObserver())->onComment(function($entry) use($relations) { - $e = new JsonResource("event", $entry->getId()); - $a = $e->attributes(); - $a->set("type", "comment"); - $a->set("comment", $entry->getComment()); - $a->set("created", $entry->getCreatedTime()); - $relations->set($e); - })->onLogEvent(function($entry) use($relations) { - $e = new JsonResource("event", $entry->getId()); - $a = $e->attributes(); - $a->set("type", "logEvent"); - $a->set("event", $entry->getLogEventType()); - $a->set("code", $entry->getLogEventCode()); - $a->set("created", $entry->getCreatedTime()); - $relations->set($e); - })->onSetSeparator(function($entry) use($relations) { - $e = new JsonResource("event", $entry->getId()); - $a = $e->attributes(); - $a->set("type", "break"); - $a->set("created", $entry->getCreatedTime()); - $relations->set($e); - })->onSpin(function($entry) use($relations, $flags) { - $e = new JsonResource("event", $entry->getId()); - $a = $e->attributes(); - $a->set("type", "spin"); - $attrs = $entry->asArray(); - unset($attrs["tag"]); - unset($attrs["id"]); - $a->merge($attrs); - $a->set("created", $entry->getCreatedTime()); - - $tag = $entry->getTag(); - if($tag) { - $a->set("artist", PlaylistEntry::swapNames($entry->getArtist())); - if($flags && sizeof($albums = Engine::api(ILibrary::class)->search(ILibrary::ALBUM_KEY, 0, 1, $tag))) - $res = Albums::fromArray($albums, $flags)[0]; - else - $res = new JsonResource("album", $tag); - - $e->relationships()->set(new Relationship("album", $res)); - } - $relations->set($e); - })); + $relations = self::fetchEvents($id, $flags); break; case "origin": $origin = $list['origin']; @@ -360,7 +391,8 @@ public function fetchRelationship(RequestInterface $request): ResponseInterface } $response = new DocumentResponse($document); - $response->headers()->set('Content-Type', ApiServer::CONTENT_TYPE); + if(Engine::getApiVer() < 2) + $response->headers()->set('Content-Type', ApiServer::CONTENT_TYPE); return $response; } @@ -470,7 +502,7 @@ function($matches) use ($list) { // insert the tracks $events = $attrs->getOptional("events"); - if($events) { + if($events && Engine::getApiVer() < 2) { $status = ''; $window = $papi->getTimestampWindow($playlist); foreach($events as $pentry) { @@ -490,6 +522,33 @@ function($matches) use ($list) { } } + if($show->relationships()->has("events")) { + $window = $papi->getTimestampWindow($playlist); + $included = $request->requestBody()->included(); + foreach($show->relationships()->get("events")->related()->all() as $er) { + $event = $included->get("event", $er->id()); + $pentry = $event->attributes()->all(); + $entry = PlaylistEntry::fromArray($pentry); + $created = $entry->getCreated(); + if($created == "auto") { + $autoTimestamp = $papi->isNowWithinShow( + ["showdate" => $date, "showtime" => $time]); + $created = $autoTimestamp ? (new \DateTime("now"))->format(IPlaylist::TIME_FORMAT_SQL) : null; + $entry->setCreated($created); + } else if($created) { + try { + $stamp = PlaylistEntry::scrubTimestamp( + new \DateTime($created), $window); + $entry->setCreated($stamp?$stamp->format(IPlaylist::TIME_FORMAT_SQL):null); + } catch(\Exception $e) { + error_log("failed to parse timestamp: $created"); + $entry->setCreated(null); + } + } + $success = $papi->insertTrackEntry($playlist, $entry, $status); + } + } + if($playlist) { if($aid && $papi->isNowWithinShow( ["showdate" => $date, "showtime" => $time])) diff --git a/controllers/Validate.php b/controllers/Validate.php index aba07bb6..5ef3ae95 100644 --- a/controllers/Validate.php +++ b/controllers/Validate.php @@ -302,7 +302,7 @@ public function validatePlaylists() { $successd = false; if($this->doTest("duplicate playlist", $success4)) { - $response = $this->client->post('api/v1/playlist', [ + $response = $this->client->post('api/v2/playlist', [ RequestOptions::JSON => [ 'data' => [ 'type' => 'show', @@ -340,7 +340,7 @@ public function validatePlaylists() { $json->data->relationships->origin->data->id == $pid && preg_match(IPlaylist::DUPLICATE_REGEX, $json->data->attributes->name) && $json->data->attributes->airname == $airname && - sizeof($json->data->attributes->events) == 3; + sizeof($json->data->relationships->events->data) == 3; $this->showSuccess($successd1, $response); } diff --git a/docs/PlaylistEvents.md b/docs/PlaylistEvents.md index 5552bbda..bda189d4 100644 --- a/docs/PlaylistEvents.md +++ b/docs/PlaylistEvents.md @@ -20,7 +20,7 @@ airname. ### Create the show: --- ```` -POST /api/v1/playlist HTTP/1.1 +POST /api/v2/playlist HTTP/1.1 X-APIKEY: eb5e0e0b42a84531af5f257ed61505050494788d Content-Type: application/vnd.api+json @@ -41,7 +41,7 @@ Content-Type: application/vnd.api+json --- ```` HTTP/1.1 201 Created -Location: /api/v1/playlist/628 +Location: /api/v2/playlist/628 Content-Length: 0 ```` --- @@ -58,7 +58,7 @@ The value of `Location` is used as the base for the subsequent requests. ### Add a comment: --- ```` -POST /api/v1/playlist/628/events HTTP/1.1 +POST /api/v2/playlist/628/events HTTP/1.1 X-APIKEY: eb5e0e0b42a84531af5f257ed61505050494788d Content-Type: application/vnd.api+json @@ -96,7 +96,7 @@ section 7.3. ### Add a spin: --- ```` -POST /api/v1/playlist/628/events HTTP/1.1 +POST /api/v2/playlist/628/events HTTP/1.1 X-APIKEY: eb5e0e0b42a84531af5f257ed61505050494788d Content-Type: application/vnd.api+json diff --git a/docs/PlaylistImport.md b/docs/PlaylistImport.md index 869e3d9c..1051b93d 100644 --- a/docs/PlaylistImport.md +++ b/docs/PlaylistImport.md @@ -21,7 +21,7 @@ airname. --- ```` -POST /api/v1/playlist HTTP/1.1 +POST /api/v2/playlist HTTP/1.1 X-APIKEY: eb5e0e0b42a84531af5f257ed61505050494788d Content-Type: application/vnd.api+json @@ -29,45 +29,66 @@ Content-Type: application/vnd.api+json "data": { "type": "show", "attributes": { - "name": "example show", - "date": "2022-01-31", - "time": "1700-1900", - "airname": "Jim", - "events": [{ - "type": "comment", - "comment": "welcome to the show", - "created": "17:00:00" - }, { - "type": "spin", - "artist": "Calla", - "track": "Elsewhere", - "album": "Calla", - "label": "Arena Rock Recording Co.", - "created": "17:01:00", - "xa:relationships": { - "album": { - "data": { - "type": "album", - "id": "1060007" - } - } - } - }] + "name": "Example Playlist", + "date": "2022-01-01", + "time": "1800-2000", + "airname": "Sample DJ" + }, + "relationships": { + "events": { + "data": [{ + "type": "event", + "id": "1" + }, { + "type": "event", + "id": "2" + }] + } } - } + }, + "included": [{ + "type": "event", + "id": "1", + "attributes": { + "type": "spin", + "artist": "example artist", + "track": "example track", + "album": "example album", + "label": "example label", + "created": "2022-01-01 18:00:00" + } + }, { + "type": "event", + "id": "2", + "attributes": { + "type": "spin", + "track": "another track", + "created": "2022-01-01 18:02:30" + }, + "relationships": { + "album": { + "data": { + "type": "album", + "id": "1060007" + } + } + } + }] } ```` --- -The album tag, if any, is specified in the xa:relationships -element. For more information, see the [Complex Attribute -Extension](xa.md). +The album tag, if any, is specified in the relationships stanza of the +included event. If an album tag is supplied, it is unnecessary to +specify the `album` or `label` attributes, or the `artist` attribute +for non-compilations, as these will be populated automatically from +the album record. ### The server responds: --- ```` HTTP/1.1 201 Created -Location: /api/v1/playlist/921 +Location: /api/v2/playlist/921 Content-Length: 0 ```` --- diff --git a/docs/Playlists.md b/docs/Playlists.md index a0775ef4..9a6dfa9e 100644 --- a/docs/Playlists.md +++ b/docs/Playlists.md @@ -3,10 +3,15 @@ This is specific API information for the 'playlist' type. For generic API information, see the [JSON:API main page](./API.md). +The current playlist API is v2. Full compatibility with previous API +versions is provided at the legacy endpoints. Legacy API behaviour +is no longer documented nor encouraged for new development. + + ### Retrieval -Retrieval is via GET request to `api/v1/playlist` (filter/pagination) or -`api/v1/playlist/:id`, where :id is the id of a specific playlist. See +Retrieval is via GET request to `api/v2/playlist` (filter/pagination) or +`api/v2/playlist/:id`, where :id is the id of a specific playlist. See below for a list of possible filter options. An [example playlist document](Samples.md#playlist) is available here. @@ -21,30 +26,13 @@ be found here. * airname * expires (expiration date, present only for deleted playlists) * rebroadcast -* events -- array of zero or more: - * type (one of `break`, `comment`, `logEvent`, `spin`) - * created - * artist - * album - * track - * xa:relationships** - * comment - * event - * code - -(** See https://github.com/RocketMan/zookeeper/pull/263 for a discussion -of the 'xa:relationships' attribute.) +* events (deprecated API version 2; use `events` relation) ### Relations * origin (to-one) -* albums (to-many) * events (to-many) - -Events are included as attributes rather than relations in the -playlist resource object. To fetch events as relations, issue a GET -request to `api/v1/playlist/:id/events`. See the -[Events Relationship](#events) section below for more information. +* albums (deprecated API version 2) ### Filters @@ -71,15 +59,29 @@ Pagination is supported only for match. Sorting is not supported. ### Insert -To insert a new playlist, issue a POST to `api/v1/playlist`. Playlist +To insert a new playlist, issue a POST to `api/v2/playlist`. Playlist details are in the request body in the same format returned by GET. X-APIKEY authentication required. +Once the playlist has been created, use of the `api/v2/playlist/:id/events` +endpoint is the preferred way to add events to the playlist. See +[Events Relationship](#events) below for details. + +Alternatively, you may specify optional events to insert into the +playlist at creation time through a process known as 'sideloading': +Include an `events` relationship in the playlist which references +objects of type `event` in the `included` stanza. Assign a locally +generated ID for each event. The locally generated ID is used only to +match the included object to the relationship; on success, it will be +discarded and replaced with a new, server-generated GUID. See [this +example](PlaylistImport.md). + If you belong to 'v' group, you may insert playlists on behalf of other users: You will own the list in these cases (i.e., can update or delete them), but they will display publicly under the other user's airname. + ### Duplicate Duplicate is identical to Insert, except that in the request body, @@ -107,7 +109,7 @@ Example: To duplicate playlist 12345 for rebroadcast on 2022-01-01 from 1800-2000: ```` -POST /api/v1/playlist HTTP/1.1 +POST /api/v2/playlist HTTP/1.1 X-APIKEY: eb5e0e0b42a84531af5f257ed61505050494788d Content-Type: application/vnd.api+json @@ -138,7 +140,7 @@ this playlist, from 0100-0200, for rebroadcast on 2022-01-01 from 1800-1900: ```` -POST /api/v1/playlist HTTP/1.1 +POST /api/v2/playlist HTTP/1.1 X-APIKEY: eb5e0e0b42a84531af5f257ed61505050494788d Content-Type: application/vnd.api+json @@ -173,7 +175,7 @@ them), but they will display publicly under the other user's airname. ### Update Update the playlist with id :id by issuing a PATCH request to -`api/v1/playlist/:id`. Playlist details are in the request body in same +`api/v2/playlist/:id`. Playlist details are in the request body in same format returned by GET. Attributes not specified in the PATCH request remain unchanged. X-APIKEY authentication required; update will fail if you do not own the playlist. @@ -184,14 +186,14 @@ may be added, updated, or deleted via the [Events Relationship](#events) (see be ### Delete Delete playlist with :id by issuing a DELETE request to -`api/v1/playlist/:id`. X-APIKEY authentication required. +`api/v2/playlist/:id`. X-APIKEY authentication required. Delete will fail if you do not own the playlist. ### Restore DELETE soft-deletes a playlist for 30 days prior to its being deleted permanently. During this period, a deleted playlist with id :id can -be restored by sending a PATCH request to `api/v1/playlist/:id`. +be restored by sending a PATCH request to `api/v2/playlist/:id`. The request body must include a single request object with at minimum `type` and `id` members, per [section @@ -208,29 +210,34 @@ own the playlist. ## Events Relationship -Events appear as attributes of a playlist. In addition, they are -exposed via the 'events' relationship, where they may be individually -added, updated, or deleted. - -When events are accessed as a relationship, each one has a unique 'id'. -To get the list of events with id's, issue a GET request to the -endpoint: - - api/v1/playlist/:id/events +You may add new events, update existing events, or delete events via +the endpoint: -where :id is the playlist id. A [sample events -document](Samples.md#events) returned by this endpoint is available -here. + api/v2/playlist/:id/events -You may add new events, update existing events, or delete events via -this same endpoint: +where :id is the playlist id. * To add a new event, issue a POST to the endpoint; * To modify an event, issue a PATCH to the endpoint; * To delete an event, issue a DELETE to the endpoint. In all cases, the request body contains a single event in the format -returned by a GET request to the endpoint. +returned by a GET request to the endpoint. It may contain the following +attributes: + +* type - one of `spin`, `break`, `comment`, `logEvent` +* artist (for type `spin`) +* track (for type `spin`) +* album (for type `spin`) +* label (for type `spin`) +* comment (for type `comment`) +* event (for type `logEvent`) +* code (for type `logEvent`) +* created (see [Automatic timestamping](#timestamping), below) + +In addition, it may contain the following relationship: + +* album (for type `spin`) For POST, upon success, you will receive an HTTP `200 OK` response; the response body will contain a resource object with the id of the @@ -238,13 +245,14 @@ created event. For PATCH and DELETE, upon success, you will receive an HTTP `204 No Content` response. If you wish, you may also use the endpoint -`api/v1/playlist/:id/relationships/events` for event addition, +`api/v2/playlist/:id/relationships/events` for event addition, modification, and deletion. The request and response semantics, as well as the server action are the same. X-APIKEY authentication is required; the operation will fail if you do not own the playlist. + ### Automatic timestamping Events added to, or updated in a 'live' (currently on-air) playlist, @@ -255,11 +263,13 @@ attribute, or a `created` attribute whose value is 'auto' in a POST request to the `api/v1/playlist/:id/events` endpoint, Zookeeper Online will automatically apply a timestamp to the new event, if the playlist is currently on-air; -* POST (API version 1.1 and later): If you supply a `created` attribute -with value 'auto' in a POST request to the `api/v1.1/playlist/:id/events` -endpoint, Zookeeper Online will automatically apply a timestamp to the new event, -if the playlist is currently on-air. Unlike API version 1, an empty -or absent `created` attribute will **not** timestamp the event; +* POST (API version 1.1 and later): If you supply a `created` +attribute with value 'auto' in a POST request to the +`api/v1.1/playlist/:id/events` or `api/v2/playlist/:id/events` +endpoint, Zookeeper Online will automatically apply a timestamp to the +new event, if the playlist is currently on-air. Unlike API version 1, +an empty or absent `created` attribute will **not** timestamp the +event; * PATCH (all API versions): If you supply a `created` attribute with value 'auto' in a PATCH request to the events endpoint, Zookeeper Online will timestamp the existing event, if the playlist is currently on-air. @@ -281,7 +291,7 @@ To move event with ID 12345 to the position currently occupied by the event with ID 67890 in playlist 98765: ```` -PATCH /api/v1/playlist/98765/events HTTP/1.1 +PATCH /api/v2/playlist/98765/events HTTP/1.1 X-APIKEY: eb5e0e0b42a84531af5f257ed61505050494788d Content-Type: application/vnd.api+json diff --git a/docs/Samples.md b/docs/Samples.md index 42b68883..9deb7d32 100644 --- a/docs/Samples.md +++ b/docs/Samples.md @@ -147,7 +147,7 @@ The following are sample documents for each of the data types. } ```` --- -### sample playlist document: +### sample playlist document (with `include=events`): ```` { @@ -159,158 +159,254 @@ The following are sample documents for each of the data types. "date": "2021-12-23", "time": "2100-2200", "airname": "DJ Away", - "rebroadcast": true, - "events": [{ + "rebroadcast": true + }, + "relationships": { + "origin": { + "links": { + "related": "/api/v1/playlist/42667/origin" + }, + "data": { + "type": "show", + "id": "41522" + } + }, + "events": { + "links": { + "related": "/api/v1/playlist/42667/events" + }, + "data": [{ + "type": "event", + "id": "818834" + }, { + "type": "event", + "id": "818835" + }, { + "type": "event", + "id": "818836" + }, { + "type": "event", + "id": "818837" + }, { + "type": "event", + "id": "818838" + }, { + "type": "event", + "id": "818839" + }, { + "type": "event", + "id": "818840" + }, { + "type": "event", + "id": "818841" + }, { + "type": "event", + "id": "818842" + }, { + "type": "event", + "id": "818843" + }, { + "type": "event", + "id": "818844" + }, { + "type": "event", + "id": "818845" + }, { + "type": "event", + "id": "818846" + }, { + "type": "event", + "id": "818847" + }, { + "type": "event", + "id": "818848" + }] + } + }, + "included": [{ + "type": "event", + "id": "818834", + "attributes": { "type": "comment", "comment": "Rebroadcast of an episode originally aired on May 20, 2021.", "created": null - }, { + } + }, { + "type": "event", + "id": "818835", + "attributes": { + "type": "spin", "artist": "Marika Papagika", "track": "Smyrneiko Minore", "album": "I Believe I'll Go Back Home: 1906\u20131959", "label": "Mississippi", - "created": "21:00:00", - "type": "spin" - }, { + "created": "21:00:00" + } + }, { + "type": "event", + "id": "818836", + "attributes": { + "type": "spin", "artist": "His Name Is Alive", "track": "Liadin", "album": "Hope Is a Candle", "label": "Disciples", - "created": "21:04:00", - "type": "spin" - }, { + "created": "21:04:00" + } + }, { + "type": "event", + "id": "818837", + "attributes": { + "type": "spin", "artist": "The Durutti Column", "track": "Weakness and Fever (originally released as a 7\" single)", "album": "LC (Reissue)", "label": "Factory Benelux", - "created": "21:07:00", - "type": "spin" - }, { + "created": "21:07:00" + } + }, { + "type": "event", + "id": "818838", + "attributes": { + "type": "spin", "artist": "For Against", "track": "You Only Live Twice", "album": "Aperture", "label": "Independent Project", - "created": "21:12:00", - "type": "spin", - "xa:relationships": { - "album": { - "data": { - "type": "album", - "id": "118820" - } + "created": "21:12:00" + }, + "relationships": { + "album": { + "data": { + "type": "album", + "id": "118820" } } - }, { + } + }, { + "type": "event", + "id": "818839", + "attributes": { + "type": "spin", "artist": "Andrew Weathers & Hayden Pedigo", "track": "Tomorrow Is the Song I Sing", "album": "Big Tex, Here We Come", "label": "Debacle", - "created": "21:16:00", - "type": "spin" - }, { + "created": "21:16:00" + } + }, { + "type": "event", + "id": "818840", + "attributes": { + "type": "spin", "artist": "Souled American", "track": "Dark as a Dungeon", "album": "Sonny", "label": "Rough Trade", - "created": "21:21:00", - "type": "spin" - }, { + "created": "21:21:00" + } + }, { + "type": "event", + "id": "818841", + "attributes": { "type": "break", "created": null - }, { + } + }, { + "type": "event", + "id": "818842", + "attributes": { + "type": "spin", "artist": "Hey Exit", "track": "Last Harvest", "album": "Eulogy for Land", "label": "Full Spectrum", - "created": "21:26:00", - "type": "spin" - }, { + "created": "21:26:00" + } + }, { + "type": "event", + "id": "818843", + "attributes": { + "type": "spin", "artist": "Calla", "track": "Elsewhere", "album": "Calla", "label": "Arena Rock Recording Co.", - "created": "21:34:00", - "type": "spin", - "xa:relationships": { - "album": { - "data": { - "type": "album", - "id": "1060007" - } + "created": "21:34:00" + }, + "relationships": { + "album": { + "data": { + "type": "album", + "id": "1060007" } } - }, { + } + }, { + "type": "event", + "id": "818844", + "attributes": { + "type": "spin", "artist": "claire rousay", "track": "discrete (the market)", "album": "a softer focus", "label": "American Dreams", - "created": "21:39:00", - "type": "spin" - }, { + "created": "21:39:00" + } + }, { + "type": "event", + "id": "818845", + "attributes": { + "type": "spin", "artist": "Jusell, Prymek, Sage, Shiroishi", "track": "Flower Clock", "album": "Yamawarau (\u5c71\u7b11\u3046)", "label": "cachedmedia", - "created": "21:45:00", - "type": "spin" - }, { + "created": "21:45:00" + } + }, { + "type": "event", + "id": "818846", + "attributes": { + "type": "spin", "artist": "Rolf Lislevand", "track": "Santiago De Murcia: Folias Gallegas", "album": "Altre Follie, 1500-1750", "label": "AliaVox", - "created": "21:49:00", - "type": "spin" - }, { + "created": "21:49:00" + } + }, { + "type": "event", + "id": "818847", + "attributes": { + "type": "spin", "artist": "Loren Mazzacane Connors", "track": "Dance Acadia", "album": "Evangeline", "label": "Road Cone", - "created": "21:52:00", - "type": "spin", - "xa:relationships": { - "album": { - "data": { - "type": "album", - "id": "463845" - } + "created": "21:52:00" + }, + "relationships": { + "album": { + "data": { + "type": "album", + "id": "463845" } } - }, { + } + }, { + "type": "event", + "id": "818848", + "attributes": { + "type": "spin", "artist": "Paul Galbraith", "track": "Sonata No. 3 BWV 1005 in D Major", "album": "The Sonatas & Partitas (arr. for 8-String Guitar)", "label": "Delos", - "created": "21:53:00", - "type": "spin" - }] - }, - "relationships": { - "origin": { - "links": { - "related": "/api/v1/playlist/42667/origin" - }, - "data": { - "type": "show", - "id": "41522" - } - }, - "albums": { - "links": { - "related": "/api/v1/playlist/42667/albums" - }, - "data": [{ - "type": "album", - "id": "118820" - }, { - "type": "album", - "id": "1060007" - }, { - "type": "album", - "id": "463845" - }] + "created": "21:53:00" } - }, - "links": { + }], + "links": { "self": "/api/v1/playlist/42667" } }, @@ -320,202 +416,6 @@ The following are sample documents for each of the data types. } ```` --- -### sample playlist events document: - -```` -{ - "data": [{ - "type": "event", - "id": "818834", - "attributes": { - "type": "comment", - "comment": "Rebroadcast of an episode originally aired on May 20, 2021.", - "created": null - } - }, { - "type": "event", - "id": "818835", - "attributes": { - "type": "spin", - "artist": "Marika Papagika", - "track": "Smyrneiko Minore", - "album": "I Believe I'll Go Back Home: 1906\u20131959", - "label": "Mississippi", - "created": "21:00:00" - } - }, { - "type": "event", - "id": "818836", - "attributes": { - "type": "spin", - "artist": "His Name Is Alive", - "track": "Liadin", - "album": "Hope Is a Candle", - "label": "Disciples", - "created": "21:04:00" - } - }, { - "type": "event", - "id": "818837", - "attributes": { - "type": "spin", - "artist": "The Durutti Column", - "track": "Weakness and Fever (originally released as a 7\" single)", - "album": "LC (Reissue)", - "label": "Factory Benelux", - "created": "21:07:00" - } - }, { - "type": "event", - "id": "818838", - "attributes": { - "type": "spin", - "artist": "For Against", - "track": "You Only Live Twice", - "album": "Aperture", - "label": "Independent Project", - "created": "21:12:00" - }, - "relationships": { - "album": { - "data": { - "type": "album", - "id": "118820" - } - } - } - }, { - "type": "event", - "id": "818839", - "attributes": { - "type": "spin", - "artist": "Andrew Weathers & Hayden Pedigo", - "track": "Tomorrow Is the Song I Sing", - "album": "Big Tex, Here We Come", - "label": "Debacle", - "created": "21:16:00" - } - }, { - "type": "event", - "id": "818840", - "attributes": { - "type": "spin", - "artist": "Souled American", - "track": "Dark as a Dungeon", - "album": "Sonny", - "label": "Rough Trade", - "created": "21:21:00" - } - }, { - "type": "event", - "id": "818841", - "attributes": { - "type": "break", - "created": null - } - }, { - "type": "event", - "id": "818842", - "attributes": { - "type": "spin", - "artist": "Hey Exit", - "track": "Last Harvest", - "album": "Eulogy for Land", - "label": "Full Spectrum", - "created": "21:26:00" - } - }, { - "type": "event", - "id": "818843", - "attributes": { - "type": "spin", - "artist": "Calla", - "track": "Elsewhere", - "album": "Calla", - "label": "Arena Rock Recording Co.", - "created": "21:34:00" - }, - "relationships": { - "album": { - "data": { - "type": "album", - "id": "1060007" - } - } - } - }, { - "type": "event", - "id": "818844", - "attributes": { - "type": "spin", - "artist": "claire rousay", - "track": "discrete (the market)", - "album": "a softer focus", - "label": "American Dreams", - "created": "21:39:00" - } - }, { - "type": "event", - "id": "818845", - "attributes": { - "type": "spin", - "artist": "Jusell, Prymek, Sage, Shiroishi", - "track": "Flower Clock", - "album": "Yamawarau (\u5c71\u7b11\u3046)", - "label": "cachedmedia", - "created": "21:45:00" - } - }, { - "type": "event", - "id": "818846", - "attributes": { - "type": "spin", - "artist": "Rolf Lislevand", - "track": "Santiago De Murcia: Folias Gallegas", - "album": "Altre Follie, 1500-1750", - "label": "AliaVox", - "created": "21:49:00" - } - }, { - "type": "event", - "id": "818847", - "attributes": { - "type": "spin", - "artist": "Loren Mazzacane Connors", - "track": "Dance Acadia", - "album": "Evangeline", - "label": "Road Cone", - "created": "21:52:00" - }, - "relationships": { - "album": { - "data": { - "type": "album", - "id": "463845" - } - } - } - }, { - "type": "event", - "id": "818848", - "attributes": { - "type": "spin", - "artist": "Paul Galbraith", - "track": "Sonata No. 3 BWV 1005 in D Major", - "album": "The Sonatas & Partitas (arr. for 8-String Guitar)", - "label": "Delos", - "created": "21:53:00" - } - }], - "links": { - "self": "/api/v1/playlist/42667/events" - }, - "jsonapi": { - "version": "1.0" - } -} -```` ---- ### sample review document: ```` {