]> git.localhorst.tv Git - alttp.git/commitdiff
pull sg crew into schedule
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Mon, 20 Feb 2023 17:30:40 +0000 (18:30 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Mon, 20 Feb 2023 17:30:40 +0000 (18:30 +0100)
14 files changed:
app/Console/Commands/SyncSpeedGaming.php
app/Http/Controllers/EpisodeController.php
app/Models/Episode.php
app/Models/EpisodeCrew.php [new file with mode: 0644]
database/migrations/2023_02_17_153122_create_episode_players_table.php
database/migrations/2023_02_20_155005_create_episode_crews_table.php [new file with mode: 0644]
resources/js/components/episodes/Crew.js [new file with mode: 0644]
resources/js/components/episodes/CrewMember.js [new file with mode: 0644]
resources/js/components/episodes/Item.js
resources/js/components/episodes/Player.js
resources/js/helpers/Crew.js [new file with mode: 0644]
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/sass/episodes.scss

index 13eb3a30e8b1724857ee4492e692962f21d9796f..d75da5078bfea496aea043793149395154959b61 100644 (file)
@@ -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'])) {
index 2fc5f28867dbdb177e8e249ec1c7d0ec800af9d6..b59ea18cbc501265265e94626aabd97996bab9c5 100644 (file)
@@ -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();
        }
 
index ddd92a6dd111e18715ca938101c79271720e59f6..15af89cf76f715135d299f00ec902ec27cd9f5f2 100644 (file)
@@ -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 (file)
index 0000000..1ec042f
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class EpisodeCrew extends Model
+{
+       use HasFactory;
+
+       public function channel() {
+               return $this->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',
+       ];
+
+}
index 0739904581be3814a8d7558e407250792f503770..90c1071d7e8295015f9abc0eb1298ff1e73edb37 100644 (file)
@@ -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 (file)
index 0000000..15af713
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        *
+        * @return void
+        */
+       public function up()
+       {
+               Schema::create('episode_crews', function (Blueprint $table) {
+                       $table->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 (file)
index 0000000..2343665
--- /dev/null
@@ -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 <Row className="episode-crew">
+               {commentators.length ?
+                       <Col>
+                               <div className="fs-5">
+                                       {t('episodes.commentary')}
+                               </div>
+                               {commentators.map(c =>
+                                       <CrewMember crew={c} key={c.id} />
+                               )}
+                       </Col>
+               : null}
+               {trackers.length ?
+                       <Col>
+                               <div className="fs-5">
+                                       {t('episodes.tracking')}
+                               </div>
+                               {trackers.map(c =>
+                                       <CrewMember crew={c} key={c.id} />
+                               )}
+                       </Col>
+               : null}
+               {techies.length ?
+                       <Col>
+                               <div className="fs-5">
+                                       {t('episodes.setup')}
+                               </div>
+                               {techies.map(c =>
+                                       <CrewMember crew={c} key={c.id} />
+                               )}
+                       </Col>
+               : null}
+       </Row>;
+};
+
+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 (file)
index 0000000..96eb1b1
--- /dev/null
@@ -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 <Button
+               className={classNames.join(' ')}
+               href={getStreamLink(crew) || null}
+               key={crew.id}
+               rel="noreferer"
+               variant="outline-twitch"
+       >
+               <img alt="" src={getAvatarUrl(crew.user)} />
+               <span>{getName(crew)}</span>
+       </Button>;
+};
+
+CrewMember.propTypes = {
+       crew: PropTypes.shape({
+               confirmed: PropTypes.bool,
+               id: PropTypes.number,
+               user: PropTypes.shape({
+               }),
+       }),
+};
+
+export default CrewMember;
index 3a3afa44f8c548b38ba02ebe97d5045a22c65ce7..2078c6dc6573bb74f554032ae67a3fdc0323cd3d 100644 (file)
@@ -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}
                                </div>
                        </div>
-                       {episode.players ?
+                       {episode.players && episode.players.length ?
                                <Players players={episode.players} />
                        : null}
+                       {episode.crew && episode.crew.length ?
+                               <Crew crew={episode.crew} />
+                       : null}
                </div>
        </div>;
 };
@@ -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,
                }),
index 27f5dd4e181e35625adda5ced29cc44f64015b0c..4fdc591dff098e28e407513b6a822e694b7f27a0 100644 (file)
@@ -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 <div className="episode-player my-3">
                <Button
diff --git a/resources/js/helpers/Crew.js b/resources/js/helpers/Crew.js
new file mode 100644 (file)
index 0000000..e60210f
--- /dev/null
@@ -0,0 +1,30 @@
+export const compareCrew = (a, b) => {
+       const a_confirmed = !!(a && a.confirmed);
+       const b_confirmed = !!(b && b.confirmed);
+       if (a_confirmed === b_confirmed) {
+               return getName(a).localeCompare(getName(b));
+       }
+       return a_confirmed ? -1 : 1;
+};
+
+export const getName = crew => {
+       if (!crew) return '';
+       if (crew.name_override) {
+               return crew.name_override;
+       }
+       if (crew.user) {
+               return crew.user.nickname || crew.user.username;
+       }
+       return '';
+};
+
+export const getStreamLink = crew => {
+       if (crew.stream_override) {
+               return crew.stream_override;
+       }
+       if (crew.user && crew.user.stream_link) {
+               return crew.user.stream_link;
+       }
+       return '';
+};
+
index 753efc651315fb89fc22aaec530f29635761b386..aff46ec38e07be24b73344c1d5810a25eb0005d6 100644 (file)
@@ -262,6 +262,11 @@ export default {
                        search: 'Suche',
                        settings: 'Einstellungen',
                },
+               episodes: {
+                       commentary: 'Kommentar',
+                       setup: 'Setup',
+                       tracking: 'Tracking',
+               },
                error: {
                        403: {
                                description: 'So aber nicht',
index cb70f5b0157bfa3ba226385c022b1abf5093c126..cb63f602c56070123c1982ba532f115bd794689c 100644 (file)
@@ -262,6 +262,11 @@ export default {
                        search: 'Search',
                        settings: 'Settings',
                },
+               episodes: {
+                       commentary: 'Commentary',
+                       setup: 'Setup',
+                       tracking: 'Tracking',
+               },
                error: {
                        403: {
                                description: 'Um no',
index fb34fa5876b201a669d33c31de56432f99067a99..3e801d44e1452604b813f88550d714fd8092d944 100644 (file)
                        vertical-align: middle;
                }
        }
+
+       .crew-member {
+               border: none;
+
+               img {
+                       max-height: 2rem;
+                       width: auto;
+                       border-radius: 50%;
+                       margin: 0 0.25rem 0 0;
+               }
+               span {
+                       vertical-align: middle;
+               }
+
+               &.unconfirmed {
+                       opacity: 0.25;
+                       &:hover {
+                               opacity: 1;
+                       }
+               }
+       }
 }