From: Daniel Karbach Date: Mon, 20 Feb 2023 17:30:40 +0000 (+0100) Subject: pull sg crew into schedule X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=e2af94795e9d8e8a2eb8c272201b4e54ebb130f0;hp=fb9c7a3524a5fc879bb062b343ec2f8fc3817f42;p=alttp.git pull sg crew into schedule --- diff --git a/app/Console/Commands/SyncSpeedGaming.php b/app/Console/Commands/SyncSpeedGaming.php index 13eb3a3..d75da50 100644 --- a/app/Console/Commands/SyncSpeedGaming.php +++ b/app/Console/Commands/SyncSpeedGaming.php @@ -5,6 +5,7 @@ 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\Organization; @@ -124,6 +125,42 @@ class SyncSpeedGaming extends Command { $episode->channels()->syncWithoutDetaching($channelIds); } + $this->purgeCrew($episode, $sgEntry['broadcasters'], 'brd'); + foreach ($sgEntry['broadcasters'] as $sgCrew) { + try { + $this->syncCrew($episode, $sgCrew, 'brd', 'setup'); + } catch (Exception $e) { + $this->error('error syncing broadcaster '.$sgCrew['id'].': '.$e->getMessage()); + } + } + + $this->purgeCrew($episode, $sgEntry['commentators'], 'comm'); + foreach ($sgEntry['commentators'] as $sgCrew) { + try { + $this->syncCrew($episode, $sgCrew, 'comm', 'commentary'); + } catch (Exception $e) { + $this->error('error syncing commentator '.$sgCrew['id'].': '.$e->getMessage()); + } + } + + $this->purgeCrew($episode, $sgEntry['helpers'], 'help'); + foreach ($sgEntry['helpers'] as $sgCrew) { + try { + $this->syncCrew($episode, $sgCrew, 'help', 'setup'); + } catch (Exception $e) { + $this->error('error syncing helper '.$sgCrew['id'].': '.$e->getMessage()); + } + } + + $this->purgeCrew($episode, $sgEntry['trackers'], 'track'); + foreach ($sgEntry['trackers'] as $sgCrew) { + try { + $this->syncCrew($episode, $sgCrew, 'track', 'tracking'); + } catch (Exception $e) { + $this->error('error syncing tracker '.$sgCrew['id'].': '.$e->getMessage()); + } + } + $this->purgePlayers($episode, $sgEntry); foreach ($sgEntry['match1']['players'] as $sgPlayer) { try { @@ -161,6 +198,47 @@ class SyncSpeedGaming extends Command { return $channel; } + private function purgeCrew(Episode $episode, $sgCrews, $prefix) { + $ext_ids = []; + foreach ($sgCrews as $sgCrew) { + $ext_ids[] = 'sg:'.$prefix.':'.$sgCrew['id']; + } + $episode->crew()->where('ext_id', 'LIKE', 'sg:'.$prefix.':%')->whereNotIn('ext_id', $ext_ids)->delete(); + } + + private function syncCrew(Episode $episode, $sgCrew, $prefix, $role) { + $ext_id = 'sg:'.$prefix.':'.$sgCrew['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->getUserBySGPlayer($sgCrew); + if ($user) { + $crew->user()->associate($user); + } else { + $crew->user()->disassociate(); + } + if ($role == 'commentary') { + $channel = $this->getChannelByCrew($episode, $sgCrew); + if ($channel) { + $crew->channel()->associate($channel); + } else { + $crew->channel()->disassociate(); + } + } + $crew->role = $role; + $crew->confirmed = $sgCrew['approved'] ?: false; + if (!empty($sgCrew['displayName'])) { + $crew->name_override = $sgCrew['displayName']; + } + if (!empty($sgCrew['publicStream'])) { + $crew->stream_override = 'https://twitch.tv/'.strtolower($sgCrew['publicStream']); + } + $crew->save(); + } + private function purgePlayers(Episode $episode, $sgEntry) { $ext_ids = []; foreach ($sgEntry['match1']['players'] as $sgPlayer) { @@ -180,15 +258,6 @@ class SyncSpeedGaming extends Command { $user = $this->getUserBySGPlayer($sgPlayer); if ($user) { $player->user()->associate($user); - if (empty($user->stream_link)) { - if (!empty($sgPlayer['publicStream'])) { - $user->stream_link = 'https://twitch.tv/'.strtolower($sgPlayer['publicStream']); - $user->save(); - } else if (!empty($sgPlayer['streamingFrom'])) { - $user->stream_link = 'https://twitch.tv/'.strtolower($sgPlayer['streamingFrom']); - $user->save(); - } - } } else { $player->user()->disassociate(); } @@ -196,15 +265,36 @@ class SyncSpeedGaming extends Command { $player->name_override = $sgPlayer['displayName']; } if (!empty($sgPlayer['streamingFrom'])) { - $player->stream_override = strtolower($sgPlayer['streamingFrom']); + $player->stream_override = 'https://twitch.tv/'.strtolower($sgPlayer['streamingFrom']); } $player->save(); } + private function getChannelByCrew(Episode $episode, $sgCrew) { + $channel = $episode->channels() + ->where('ext_id', 'LIKE', 'sg:%') + ->whereJsonContains('languages', $sgCrew['language']) + ->first(); + if ($channel) { + return $channel; + } + } + private function getUserBySGPlayer($player) { if (!empty($player['discordId'])) { $user = User::find($player['discordId']); - if ($user) return $user; + if ($user) { + if (empty($user->stream_link)) { + if (!empty($sgPlayer['publicStream'])) { + $user->stream_link = 'https://twitch.tv/'.strtolower($sgPlayer['publicStream']); + $user->save(); + } else if (!empty($sgPlayer['streamingFrom'])) { + $user->stream_link = 'https://twitch.tv/'.strtolower($sgPlayer['streamingFrom']); + $user->save(); + } + } + return $user; + } DiscordBotCommand::syncUser($player['discordId']); } if (!empty($player['discordTag'])) { diff --git a/app/Http/Controllers/EpisodeController.php b/app/Http/Controllers/EpisodeController.php index 2fc5f28..b59ea18 100644 --- a/app/Http/Controllers/EpisodeController.php +++ b/app/Http/Controllers/EpisodeController.php @@ -25,6 +25,15 @@ class EpisodeController extends Controller ->where('events.visible', '=', true) ->orderBy('episodes.start') ->limit(1000); + if ($request->user() && $request->user()->isAdmin()) { + $episodes = $episodes->with('crew'); + } else { + $episodes = $episodes->with([ + 'crew' => function ($query) { + $query->where('confirmed', true); + } + ]); + } return $episodes->get()->toJson(); } diff --git a/app/Models/Episode.php b/app/Models/Episode.php index ddd92a6..15af89c 100644 --- a/app/Models/Episode.php +++ b/app/Models/Episode.php @@ -14,6 +14,10 @@ class Episode extends Model return $this->belongsToMany(Channel::class); } + public function crew() { + return $this->hasMany(EpisodeCrew::class); + } + public function event() { return $this->belongsTo(Event::class); } diff --git a/app/Models/EpisodeCrew.php b/app/Models/EpisodeCrew.php new file mode 100644 index 0000000..1ec042f --- /dev/null +++ b/app/Models/EpisodeCrew.php @@ -0,0 +1,34 @@ +belongsTo(Channel::class); + } + + public function episode() { + return $this->belongsTo(Episode::class); + } + + public function user() { + return $this->belongsTo(User::class); + } + + protected $casts = [ + 'confirmed' => 'boolean', + ]; + + protected $hidden = [ + 'created_at', + 'ext_id', + 'updated_at', + ]; + +} diff --git a/database/migrations/2023_02_17_153122_create_episode_players_table.php b/database/migrations/2023_02_17_153122_create_episode_players_table.php index 0739904..90c1071 100644 --- a/database/migrations/2023_02_17_153122_create_episode_players_table.php +++ b/database/migrations/2023_02_17_153122_create_episode_players_table.php @@ -6,31 +6,31 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { - /** - * Run the migrations. - * - * @return void - */ - public function up() - { - Schema::create('episode_players', function (Blueprint $table) { - $table->id(); + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::create('episode_players', function (Blueprint $table) { + $table->id(); $table->foreignId('episode_id')->constrained(); $table->foreignId('user_id')->nullable()->default(null)->constrained(); $table->string('name_override')->default(''); $table->string('stream_override')->default(''); $table->string('ext_id')->nullable()->default(null); - $table->timestamps(); - }); - } + $table->timestamps(); + }); + } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('episode_players'); - } + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('episode_players'); + } }; diff --git a/database/migrations/2023_02_20_155005_create_episode_crews_table.php b/database/migrations/2023_02_20_155005_create_episode_crews_table.php new file mode 100644 index 0000000..15af713 --- /dev/null +++ b/database/migrations/2023_02_20_155005_create_episode_crews_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('episode_id')->constrained(); + $table->foreignId('user_id')->nullable()->default(null)->constrained(); + $table->foreignId('channel_id')->nullable()->default(null)->constrained(); + $table->string('role')->default('commentary'); + $table->boolean('confirmed')->default(false); + $table->string('name_override')->default(''); + $table->string('stream_override')->default(''); + $table->string('ext_id')->nullable()->default(null); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('episode_crews'); + } +}; diff --git a/resources/js/components/episodes/Crew.js b/resources/js/components/episodes/Crew.js new file mode 100644 index 0000000..2343665 --- /dev/null +++ b/resources/js/components/episodes/Crew.js @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Col, Row } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import CrewMember from './CrewMember'; +import { compareCrew } from '../../helpers/Crew'; + +const Crew = ({ crew }) => { + const { t } = useTranslation(); + + const commentators = React.useMemo(() => + crew.filter(c => c.role === 'commentary').sort(compareCrew) + , [crew]); + const trackers = React.useMemo(() => + crew.filter(c => c.role === 'tracking').sort(compareCrew) + , [crew]); + const techies = React.useMemo(() => + crew.filter(c => c.role === 'setup').sort(compareCrew) + , [crew]); + + return + {commentators.length ? + +
+ {t('episodes.commentary')} +
+ {commentators.map(c => + + )} + + : null} + {trackers.length ? + +
+ {t('episodes.tracking')} +
+ {trackers.map(c => + + )} + + : null} + {techies.length ? + +
+ {t('episodes.setup')} +
+ {techies.map(c => + + )} + + : null} +
; +}; + +Crew.propTypes = { + crew: PropTypes.arrayOf(PropTypes.shape({ + })), +}; + +export default Crew; diff --git a/resources/js/components/episodes/CrewMember.js b/resources/js/components/episodes/CrewMember.js new file mode 100644 index 0000000..96eb1b1 --- /dev/null +++ b/resources/js/components/episodes/CrewMember.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button } from 'react-bootstrap'; + +import { getName, getStreamLink } from '../../helpers/Crew'; +import { getAvatarUrl } from '../../helpers/User'; + +const CrewMember = ({ crew }) => { + const classNames = [ + 'crew-member', + 'text-light', + ]; + if (!crew.confirmed) { + classNames.push('unconfirmed'); + } + return ; +}; + +CrewMember.propTypes = { + crew: PropTypes.shape({ + confirmed: PropTypes.bool, + id: PropTypes.number, + user: PropTypes.shape({ + }), + }), +}; + +export default CrewMember; diff --git a/resources/js/components/episodes/Item.js b/resources/js/components/episodes/Item.js index 3a3afa4..2078c6d 100644 --- a/resources/js/components/episodes/Item.js +++ b/resources/js/components/episodes/Item.js @@ -4,6 +4,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import Channels from './Channels'; +import Crew from './Crew'; import Players from './Players'; const isActive = episode => { @@ -54,9 +55,12 @@ const Item = ({ episode }) => { : null} - {episode.players ? + {episode.players && episode.players.length ? : null} + {episode.crew && episode.crew.length ? + + : null} ; }; @@ -65,6 +69,8 @@ Item.propTypes = { episode: PropTypes.shape({ channels: PropTypes.arrayOf(PropTypes.shape({ })), + crew: PropTypes.arrayOf(PropTypes.shape({ + })), event: PropTypes.shape({ title: PropTypes.string, }), diff --git a/resources/js/components/episodes/Player.js b/resources/js/components/episodes/Player.js index 27f5dd4..4fdc591 100644 --- a/resources/js/components/episodes/Player.js +++ b/resources/js/components/episodes/Player.js @@ -2,28 +2,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import { Button } from 'react-bootstrap'; +import { getName, getStreamLink } from '../../helpers/Crew'; import { getAvatarUrl } from '../../helpers/User'; -const getName = player => { - if (player.name_override) { - return player.name_override; - } - if (player.user) { - return player.user.nickname || player.user.username; - } - return ''; -}; - -const getStreamLink = player => { - if (player.stream_override) { - return `https://twitch.tv/${player.stream_override}`; - } - if (player.user && player.user.stream_link) { - return player.user.stream_link; - } - return ''; -}; - const Player = ({ player }) => { return