2019-09-07 19:27:44 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
class TwitchBridge extends BridgeAbstract
|
2022-07-01 16:10:30 +03:00
|
|
|
{
|
2019-09-07 19:27:44 +03:00
|
|
|
const MAINTAINER = 'Roliga';
|
|
|
|
const NAME = 'Twitch Bridge';
|
|
|
|
const URI = 'https://twitch.tv/';
|
|
|
|
const CACHE_TIMEOUT = 300; // 5min
|
|
|
|
const DESCRIPTION = 'Twitch channel videos';
|
|
|
|
const PARAMETERS = [ [
|
|
|
|
'channel' => [
|
|
|
|
'name' => 'Channel',
|
|
|
|
'type' => 'text',
|
|
|
|
'required' => true,
|
2022-03-24 13:59:34 +03:00
|
|
|
'exampleValue' => 'criticalrole',
|
2019-09-07 19:27:44 +03:00
|
|
|
'title' => 'Lowercase channel name as seen in channel URL'
|
|
|
|
],
|
|
|
|
'type' => [
|
|
|
|
'name' => 'Type',
|
|
|
|
'type' => 'list',
|
|
|
|
'values' => [
|
|
|
|
'All' => 'all',
|
|
|
|
'Archive' => 'archive',
|
|
|
|
'Highlights' => 'highlight',
|
2020-10-30 16:50:36 +03:00
|
|
|
'Uploads' => 'upload',
|
|
|
|
'Past Premieres' => 'past_premiere',
|
|
|
|
'Premiere Uploads' => 'premiere_upload'
|
2019-09-07 19:27:44 +03:00
|
|
|
],
|
|
|
|
'defaultValue' => 'archive'
|
2022-07-01 16:10:30 +03:00
|
|
|
]
|
2019-09-07 19:27:44 +03:00
|
|
|
]];
|
|
|
|
|
2020-10-30 16:50:36 +03:00
|
|
|
const BROADCAST_TYPES = [
|
|
|
|
'all' => [
|
|
|
|
'ARCHIVE',
|
|
|
|
'HIGHLIGHT',
|
|
|
|
'UPLOAD',
|
|
|
|
'PAST_PREMIERE',
|
|
|
|
'PREMIERE_UPLOAD'
|
|
|
|
],
|
|
|
|
'archive' => 'ARCHIVE',
|
|
|
|
'highlight' => 'HIGHLIGHT',
|
|
|
|
'upload' => 'UPLOAD',
|
|
|
|
'past_premiere' => 'PAST_PREMIERE',
|
|
|
|
'premiere_upload' => 'PREMIERE_UPLOAD'
|
|
|
|
];
|
|
|
|
|
2019-09-07 19:27:44 +03:00
|
|
|
public function collectData()
|
|
|
|
{
|
2020-10-30 16:50:36 +03:00
|
|
|
$query = <<<'EOD'
|
|
|
|
query VODList($channel: String!, $types: [BroadcastType!]) {
|
|
|
|
user(login: $channel) {
|
|
|
|
displayName
|
|
|
|
videos(types: $types, sort: TIME) {
|
|
|
|
edges {
|
|
|
|
node {
|
|
|
|
id
|
|
|
|
title
|
|
|
|
publishedAt
|
|
|
|
lengthSeconds
|
|
|
|
viewCount
|
|
|
|
thumbnailURLs(width: 640, height: 360)
|
|
|
|
previewThumbnailURL(width: 640, height: 360)
|
|
|
|
description
|
|
|
|
tags
|
|
|
|
contentTags {
|
|
|
|
isLanguageTag
|
|
|
|
localizedName
|
|
|
|
}
|
|
|
|
game {
|
|
|
|
displayName
|
|
|
|
}
|
|
|
|
moments(momentRequestType: VIDEO_CHAPTER_MARKERS) {
|
|
|
|
edges {
|
|
|
|
node {
|
|
|
|
description
|
|
|
|
positionMilliseconds
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
EOD;
|
2022-11-08 23:17:32 +03:00
|
|
|
$channel = $this->getInput('channel');
|
|
|
|
$type = $this->getInput('type');
|
2020-10-30 16:50:36 +03:00
|
|
|
$variables = [
|
2022-11-08 23:17:32 +03:00
|
|
|
'channel' => $channel,
|
|
|
|
'types' => self::BROADCAST_TYPES[$type]
|
2019-09-07 19:27:44 +03:00
|
|
|
];
|
2020-10-30 16:50:36 +03:00
|
|
|
$data = $this->apiRequest($query, $variables);
|
2022-11-08 23:17:32 +03:00
|
|
|
if ($data->user === null) {
|
|
|
|
throw new \Exception(sprintf('Unable to find channel `%s`', $channel));
|
|
|
|
}
|
2020-10-30 16:50:36 +03:00
|
|
|
$user = $data->user;
|
|
|
|
foreach ($user->videos->edges as $edge) {
|
|
|
|
$video = $edge->node;
|
|
|
|
|
|
|
|
$url = 'https://www.twitch.tv/videos/' . $video->id;
|
2019-09-07 19:27:44 +03:00
|
|
|
|
|
|
|
$item = [
|
2020-10-30 16:50:36 +03:00
|
|
|
'uri' => $url,
|
2019-09-07 19:27:44 +03:00
|
|
|
'title' => $video->title,
|
2020-10-30 16:50:36 +03:00
|
|
|
'timestamp' => $video->publishedAt,
|
|
|
|
'author' => $user->displayName,
|
2019-09-07 19:27:44 +03:00
|
|
|
];
|
|
|
|
|
|
|
|
// Add categories for tags and played game
|
2020-10-30 16:50:36 +03:00
|
|
|
$item['categories'] = $video->tags;
|
|
|
|
if (!is_null($video->game)) {
|
|
|
|
$item['categories'][] = $video->game->displayName;
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
2020-10-30 16:50:36 +03:00
|
|
|
foreach ($video->contentTags as $tag) {
|
|
|
|
if (!$tag->isLanguageTag) {
|
|
|
|
$item['categories'][] = $tag->localizedName;
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
|
|
|
}
|
2019-09-07 19:27:44 +03:00
|
|
|
|
|
|
|
// Add enclosures for thumbnails from a few points in the video
|
2020-10-30 16:50:36 +03:00
|
|
|
// Thumbnail list has duplicate entries sometimes so remove those
|
|
|
|
$item['enclosures'] = array_unique($video->thumbnailURLs);
|
2019-09-07 19:27:44 +03:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Content format example:
|
|
|
|
*
|
|
|
|
* [Preview Image]
|
|
|
|
*
|
|
|
|
* Some optional video description.
|
|
|
|
*
|
|
|
|
* Duration: 1:23:45
|
|
|
|
* Views: 123
|
|
|
|
*
|
|
|
|
* Played games:
|
|
|
|
* * 00:00:00 Game 1
|
|
|
|
* * 00:12:34 Game 2
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
$item['content'] = '<p><a href="'
|
2020-10-30 16:50:36 +03:00
|
|
|
. $url
|
2019-09-07 19:27:44 +03:00
|
|
|
. '"><img src="'
|
2020-10-30 16:50:36 +03:00
|
|
|
. $video->previewThumbnailURL
|
2019-09-07 19:27:44 +03:00
|
|
|
. '" /></a></p><p>'
|
2020-10-30 16:50:36 +03:00
|
|
|
. $video->description // in markdown format
|
2019-09-07 19:27:44 +03:00
|
|
|
. '</p><p><b>Duration:</b> '
|
2020-10-30 16:50:36 +03:00
|
|
|
. $this->formatTimestampTime($video->lengthSeconds)
|
2019-09-07 19:27:44 +03:00
|
|
|
. '<br/><b>Views:</b> '
|
2020-10-30 16:50:36 +03:00
|
|
|
. $video->viewCount
|
2019-09-07 19:27:44 +03:00
|
|
|
. '</p>';
|
|
|
|
|
|
|
|
// Add played games list to content
|
2020-10-30 16:50:36 +03:00
|
|
|
$item['content'] .= '<p><b>Played games:</b><ul>';
|
|
|
|
if (count($video->moments->edges) > 0) {
|
|
|
|
foreach ($video->moments->edges as $edge) {
|
|
|
|
$moment = $edge->node;
|
|
|
|
|
|
|
|
$item['categories'][] = $moment->description;
|
2019-09-07 19:27:44 +03:00
|
|
|
$item['content'] .= '<li><a href="'
|
2020-10-30 16:50:36 +03:00
|
|
|
. $url
|
2019-09-07 19:27:44 +03:00
|
|
|
. '?t='
|
2020-10-30 16:50:36 +03:00
|
|
|
. $this->formatQueryTime($moment->positionMilliseconds / 1000)
|
2019-09-07 19:27:44 +03:00
|
|
|
. '">'
|
2020-10-30 16:50:36 +03:00
|
|
|
. $this->formatTimestampTime($moment->positionMilliseconds / 1000)
|
2019-09-07 19:27:44 +03:00
|
|
|
. '</a> - '
|
2020-10-30 16:50:36 +03:00
|
|
|
. $moment->description
|
2019-09-07 19:27:44 +03:00
|
|
|
. '</li>';
|
|
|
|
}
|
2020-10-30 16:50:36 +03:00
|
|
|
} else {
|
|
|
|
$item['content'] .= '<li><a href="'
|
|
|
|
. $url
|
|
|
|
. '">00:00:00</a> - '
|
|
|
|
. ($video->game ? $video->game->displayName : 'No Game')
|
|
|
|
. '</li>';
|
2019-09-07 19:27:44 +03:00
|
|
|
}
|
|
|
|
$item['content'] .= '</ul></p>';
|
|
|
|
|
2020-10-30 16:50:36 +03:00
|
|
|
$item['categories'] = array_unique($item['categories']);
|
|
|
|
|
2019-09-07 19:27:44 +03:00
|
|
|
$this->items[] = $item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// e.g. 01:53:27
|
|
|
|
private function formatTimestampTime($seconds)
|
|
|
|
{
|
|
|
|
return sprintf(
|
|
|
|
'%02d:%02d:%02d',
|
|
|
|
floor($seconds / 3600),
|
|
|
|
($seconds / 60) % 60,
|
|
|
|
$seconds % 60
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// e.g. 01h53m27s
|
|
|
|
private function formatQueryTime($seconds)
|
|
|
|
{
|
|
|
|
return sprintf(
|
|
|
|
'%02dh%02dm%02ds',
|
|
|
|
floor($seconds / 3600),
|
|
|
|
($seconds / 60) % 60,
|
|
|
|
$seconds % 60
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-10-30 16:50:36 +03:00
|
|
|
// GraphQL: https://graphql.org/
|
|
|
|
// Tool for developing/testing queries: https://github.com/skevy/graphiql-app
|
|
|
|
private function apiRequest($query, $variables)
|
|
|
|
{
|
|
|
|
$request = [
|
|
|
|
'query' => $query,
|
|
|
|
'variables' => $variables
|
|
|
|
];
|
2022-09-09 21:18:50 +03:00
|
|
|
/**
|
|
|
|
* Official instructions for obtaining your own client ID can be found here:
|
|
|
|
* https://dev.twitch.tv/docs/v5/#getting-a-client-id
|
|
|
|
*/
|
2019-09-07 19:27:44 +03:00
|
|
|
$header = [
|
2022-09-09 21:18:50 +03:00
|
|
|
'Client-ID: kimne78kx3ncx6brgo4mv6wki5h1ko'
|
2019-09-07 19:27:44 +03:00
|
|
|
];
|
2020-10-30 16:50:36 +03:00
|
|
|
$opts = [
|
|
|
|
CURLOPT_CUSTOMREQUEST => 'POST',
|
|
|
|
CURLOPT_POSTFIELDS => json_encode($request)
|
|
|
|
];
|
2019-09-07 19:27:44 +03:00
|
|
|
|
2023-07-06 19:52:19 +03:00
|
|
|
Logger::debug("Sending GraphQL query:\n" . $query);
|
|
|
|
Logger::debug("Sending GraphQL variables:\n" . json_encode($variables, JSON_PRETTY_PRINT));
|
2022-09-09 21:18:50 +03:00
|
|
|
$response = json_decode(getContents('https://gql.twitch.tv/gql', $header, $opts));
|
2023-07-06 19:52:19 +03:00
|
|
|
Logger::debug("Got GraphQL response:\n" . json_encode($response, JSON_PRETTY_PRINT));
|
2020-10-30 16:50:36 +03:00
|
|
|
|
|
|
|
if (isset($response->errors)) {
|
|
|
|
$messages = array_column($response->errors, 'message');
|
2022-09-09 21:18:50 +03:00
|
|
|
throw new \Exception(sprintf('twitch api: `%s`', implode("\n", $messages)));
|
2020-10-30 16:50:36 +03:00
|
|
|
}
|
2019-09-07 19:27:44 +03:00
|
|
|
|
2020-10-30 16:50:36 +03:00
|
|
|
return $response->data;
|
2019-09-07 19:27:44 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getName()
|
|
|
|
{
|
|
|
|
if (!is_null($this->getInput('channel'))) {
|
|
|
|
return $this->getInput('channel') . ' twitch videos';
|
|
|
|
}
|
|
|
|
|
|
|
|
return parent::getName();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getURI()
|
|
|
|
{
|
|
|
|
if (!is_null($this->getInput('channel'))) {
|
|
|
|
return self::URI . $this->getInput('channel');
|
|
|
|
}
|
|
|
|
|
|
|
|
return parent::getURI();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function detectParameters($url)
|
|
|
|
{
|
|
|
|
$params = [];
|
|
|
|
|
|
|
|
// Matches e.g. https://www.twitch.tv/someuser/videos?filter=archives
|
|
|
|
$regex = '/^(https?:\/\/)?
|
|
|
|
(www\.)?
|
|
|
|
twitch\.tv\/
|
|
|
|
([^\/&?\n]+)
|
|
|
|
\/videos\?.*filter=
|
|
|
|
(all|archive|highlight|upload)/x';
|
|
|
|
if (preg_match($regex, $url, $matches) > 0) {
|
|
|
|
$params['channel'] = urldecode($matches[3]);
|
|
|
|
$params['type'] = $matches[4];
|
|
|
|
return $params;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|