namespace App\Console\Commands;
+use App\Models\Channel;
use App\Models\DiscordBotCommand;
use App\Models\Episode;
+use App\Models\EpisodeCrew;
use App\Models\EpisodePlayer;
use App\Models\Event;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Str;
class SyncHTH extends Command {
* @return int
*/
public function handle() {
- $events = Event::where('external_schedule', 'LIKE', 'hth:%')
+ $events = Event::query()->where('external_schedule', 'LIKE', 'hth:%')
->where(function (Builder $query) {
$query->whereNull('end');
$query->orWhere('end', '>', now());
$hthEvent = $parts[1];
$this->line('series: "'.$hthSeries.'", event: "'.$hthEvent.'"');
$hthSchedule = Http::withHeaders([
- 'X-API-Key', config('hth.api_key')
+ 'X-API-Key' => config('hth.api_key')
])->post('https://hth.zeldaspeedruns.com/api/v1/graphql', [
'operation_name' => 'my_schedule',
'variables' => [
query my_schedule(\$my_series: String, \$my_event: String) {
series(name: \$my_series) {
event(name: \$my_event) {
- races {
- id
- start
- room
- phase
- round
- game
- teams {
- name
- members {
- role
- user {
+ races {
id
- displayName
- racetimeId
- discordId
+ start
+ room
+ phase
+ round
+ game
+ teams {
+ name
+ members {
+ role
+ user {
+ id
+ displayName
+ discordId
+ }
+ }
+ }
+ restreamUrls {
+ url
+ language
+ }
+ confirmedVolunteers {
+ roleTypeName
+ user {
+ id
+ displayName
+ discordId
+ }
}
- }
}
}
}
}
- }
GRAPHQL,
])->throw()->json();
foreach ($hthSchedule['data']['series']['event']['races'] as $hthEntry) {
private function syncSchedule(Event $event, $hthEntry): void {
$ext_id = 'hth:'.$hthEntry['id'];
- $episode = Episode::firstWhere('ext_id', '=', $ext_id);
+ $episode = Episode::query()->firstWhere('ext_id', '=', $ext_id);
if (!$hthEntry['start']) {
if ($episode) {
$episode->callOff();
}
return;
}
+ if ($hthEntry['start'] < now()->sub(7, 'day')) {
+ // skip if too old
+ return;
+ }
if (!$episode) {
$episode = new Episode();
$episode->ext_id = $ext_id;
}
}
}
+
+ $first_channel = null;
+ if (!empty($hthEntry['restreamUrls'])) {
+ foreach ($hthEntry['restreamUrls'] as $restream) {
+ $channel = $this->syncChannel($episode, $restream);
+ $episode->channels()->syncWithoutDetaching([$channel->id]);
+ if (is_null($first_channel)) {
+ $first_channel = $channel;
+ }
+ }
+ }
+ $this->purgeCrew($episode, $hthEntry['confirmedVolunteers']);
+ if (!empty($hthEntry['confirmedVolunteers'])) {
+ foreach ($hthEntry['confirmedVolunteers'] as $hthCrew) {
+ $this->syncCrew($episode, $hthCrew, $first_channel);
+ }
+ }
}
private function purgePlayers(Episode $episode, $hthEntry): void {
$player->save();
}
+ private function syncChannel(Episode $episode, $hthChannel): Channel {
+ $normalized_url = Str::rtrim(Str::lower($hthChannel['url']), '/');
+ $normalized_url = str_replace('http://', 'https://', $normalized_url);
+ $normalized_url = str_replace('www.twitch.tv', 'twitch.tv', $normalized_url);
+ $channel = Channel::query()->firstWhere('stream_link', 'LIKE', $normalized_url);
+ if (!$channel) {
+ $channel = new Channel();
+ $channel->stream_link = $normalized_url;
+ $channel->title = Str::afterLast($normalized_url, '/');
+ $channel->languages = [static::$LANGUAGE_MAP[$hthChannel['language']]];
+ $channel->twitch_chat = '#'.Str::afterLast($normalized_url, '/');
+ $channel->access_key = Str::uuid();
+ $channel->save();
+ }
+ return $channel;
+ }
+
+ private function purgeCrew(Episode $episode, $hthCrews) {
+ $ext_ids = [];
+ foreach ($hthCrews as $hthCrew) {
+ $ext_ids[] = 'hth:'.$hthCrew['roleTypeName'].':'.$hthCrew['user']['id'];
+ }
+ $episode->crew()->where('ext_id', 'LIKE', 'hth:%')->whereNotIn('ext_id', $ext_ids)->delete();
+ }
+
+ private function syncCrew(Episode $episode, $hthCrew, Channel|null $channel): void {
+ $ext_id = 'hth:'.$hthCrew['roleTypeName'].':'.$hthCrew['user']['id'];
+ $crew = $episode->crew()->firstWhere('ext_id', '=', $ext_id);
+ if (!$crew) {
+ $crew = new EpisodeCrew();
+ $crew->ext_id = $ext_id;
+ $crew->episode()->associate($episode);
+ }
+ $user = $this->getUserByHTHPlayer($hthCrew);
+ if ($user) {
+ $crew->user()->associate($user);
+ } else {
+ $crew->user()->disassociate();
+ }
+ $role = static::$ROLE_MAP[$hthCrew['roleTypeName']];
+ if ($role == 'commentary') {
+ if ($channel) {
+ $crew->channel()->associate($channel);
+ } else {
+ $crew->channel()->disassociate();
+ }
+ }
+ $crew->role = $role;
+ $crew->confirmed = true;
+ if (!empty($hthCrew['user']['displayName'])) {
+ $crew->name_override = $hthCrew['user']['displayName'];
+ }
+ $crew->save();
+ }
+
private function getUserByHTHPlayer($player): User|null {
if (!empty($player['user']['discordId'])) {
- $user = User::find($player['user']['discordId']);
+ $user = User::query()->find($player['user']['discordId']);
if ($user) {
return $user;
}
DiscordBotCommand::syncUser($player['user']['discordId']);
}
if (!empty($player['user']['displayName'])) {
- $user = User::firstWhere('discord_nickname', 'LIKE', $player['user']['displayName']);
+ $user = User::query()->firstWhere('discord_nickname', 'LIKE', $player['user']['displayName']);
if ($user) {
return $user;
}
- $user = User::firstWhere('global_name', 'LIKE', $player['user']['displayName']);
+ $user = User::query()->firstWhere('global_name', 'LIKE', $player['user']['displayName']);
if ($user) {
return $user;
}
- $user = User::firstWhere('username', 'LIKE', $player['user']['displayName']);
+ $user = User::query()->firstWhere('username', 'LIKE', $player['user']['displayName']);
if ($user) {
return $user;
}
return null;
}
+ private static $LANGUAGE_MAP = [
+ 'ENGLISH' => 'en',
+ 'FRENCH' => 'fr',
+ 'GERMAN' => 'de',
+ 'PORTUGUESE' => 'pt',
+ ];
+
+ private static $ROLE_MAP = [
+ 'Commentary' => 'commentary',
+ 'Race Monitoring' => 'setup',
+ 'Tracking' => 'tracking',
+ ];
+
}