From 15132749249f6418fd5695547b5c323a0ad10939 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Mon, 20 Feb 2023 11:34:14 +0100 Subject: [PATCH] sync episode channels --- app/Console/Commands/SyncSpeedGaming.php | 53 +++++++++++++++++- app/Http/Controllers/EpisodeController.php | 2 +- app/Models/Channel.php | 26 +++++++++ app/Models/Episode.php | 11 ++++ app/Models/EpisodePlayer.php | 1 + app/Models/Event.php | 7 +++ app/Models/Organization.php | 16 ++++++ ...2_20_084120_create_organizations_table.php | 39 ++++++++++++++ ...023_02_20_084344_create_channels_table.php | 37 +++++++++++++ ...20_093249_create_channel_episode_table.php | 32 +++++++++++ resources/js/components/episodes/Channel.js | 30 +++++++++++ resources/js/components/episodes/Channels.js | 18 +++++++ resources/js/components/episodes/Item.js | 54 +++++++++++++++---- resources/sass/episodes.scss | 6 +++ 14 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 app/Models/Channel.php create mode 100644 app/Models/Organization.php create mode 100644 database/migrations/2023_02_20_084120_create_organizations_table.php create mode 100644 database/migrations/2023_02_20_084344_create_channels_table.php create mode 100644 database/migrations/2023_02_20_093249_create_channel_episode_table.php create mode 100644 resources/js/components/episodes/Channel.js create mode 100644 resources/js/components/episodes/Channels.js diff --git a/app/Console/Commands/SyncSpeedGaming.php b/app/Console/Commands/SyncSpeedGaming.php index 8c74c68..4d2cb9c 100644 --- a/app/Console/Commands/SyncSpeedGaming.php +++ b/app/Console/Commands/SyncSpeedGaming.php @@ -2,9 +2,11 @@ namespace App\Console\Commands; +use App\Models\Channel; use App\Models\Episode; use App\Models\EpisodePlayer; use App\Models\Event; +use App\Models\Organization; use App\Models\User; use Carbon\Carbon; use Illuminate\Console\Command; @@ -33,12 +35,15 @@ class SyncSpeedGaming extends Command { * @return int */ public function handle() { + $this->org = Organization::where('name', '=', 'sg')->firstOrFail(); + $events = Event::where('external_schedule', 'LIKE', 'sg:%') ->where(function (Builder $query) { $query->whereNull('end'); $query->orWhere('end', '<', now()); }) ->get(); + foreach ($events as $event) { try { $this->line('syncing '.$event->name); @@ -47,6 +52,7 @@ class SyncSpeedGaming extends Command { $this->error('error syncing event '.$event->name.': '.$e->getMessage()); } } + return 0; } @@ -101,6 +107,22 @@ class SyncSpeedGaming extends Command { $episode->confirmed = $sgEntry['approved']; $episode->comment = $sgEntry['match1']['note']; $episode->save(); + + $this->purgeChannels($episode, $sgEntry); + $channelIds = []; + foreach ($sgEntry['channels'] as $sgChannel) { + if ($sgChannel['initials'] == 'NONE') continue; + try { + $channel = $this->syncChannel($episode, $sgChannel); + $channelIds[] = $channel->id; + } catch (Exception $e) { + $this->error('error syncing channel '.$sgChannel['id'].': '.$e->getMessage()); + } + } + if (!empty($channelIds)) { + $episode->channels()->syncWithoutDetaching($channelIds); + } + $this->purgePlayers($episode, $sgEntry); foreach ($sgEntry['match1']['players'] as $sgPlayer) { try { @@ -111,6 +133,33 @@ class SyncSpeedGaming extends Command { } } + private function purgeChannels(Episode $episode, $sgEntry) { + $ext_ids = []; + foreach ($sgEntry['channels'] as $sgChannel) { + $ext_ids[] = 'sg:'.$sgChannel['id']; + } + $episode->channels() + ->where('ext_id', 'LIKE', 'sg:%') + ->whereNotIn('ext_id', $ext_ids) + ->detach(); + } + + private function syncChannel(Episode $episode, $sgChannel) { + $ext_id = 'sg:'.$sgChannel['id']; + $channel = $this->org->channels()->firstWhere('ext_id', '=', $ext_id); + if (!$channel) { + $channel = new Channel(); + $channel->ext_id = $ext_id; + $channel->organization()->associate($this->org); + } + $channel->short_name = $sgChannel['initials']; + $channel->title = $sgChannel['name']; + $channel->stream_link = 'https://twitch.tv/'.strtolower($sgChannel['name']); + $channel->languages = [$sgChannel['language']]; + $channel->save(); + return $channel; + } + private function purgePlayers(Episode $episode, $sgEntry) { $ext_ids = []; foreach ($sgEntry['match1']['players'] as $sgPlayer) { @@ -137,7 +186,7 @@ class SyncSpeedGaming extends Command { $player->name_override = $sgPlayer['displayName']; } if (!empty($sgPlayer['streamingFrom'])) { - $player->stream_override = $sgPlayer['streamingFrom']; + $player->stream_override = strtolower($sgPlayer['streamingFrom']); } $player->save(); } @@ -158,4 +207,6 @@ class SyncSpeedGaming extends Command { return null; } + private $org; + } diff --git a/app/Http/Controllers/EpisodeController.php b/app/Http/Controllers/EpisodeController.php index 6672cdc..2fc5f28 100644 --- a/app/Http/Controllers/EpisodeController.php +++ b/app/Http/Controllers/EpisodeController.php @@ -16,7 +16,7 @@ class EpisodeController extends Controller ]); $after = isset($validatedData['after']) ? $validatedData['after'] : Carbon::now()->sub(2, 'hours'); $before = isset($validatedData['before']) ? $validatedData['before'] : Carbon::now()->add(1, 'days'); - $episodes = Episode::with(['event', 'players', 'players.user']) + $episodes = Episode::with(['channels', 'event', 'players', 'players.user']) ->select('episodes.*') ->join('events', 'episodes.event_id', '=', 'events.id') ->where('episodes.confirmed', '=', true) diff --git a/app/Models/Channel.php b/app/Models/Channel.php new file mode 100644 index 0000000..d68ba58 --- /dev/null +++ b/app/Models/Channel.php @@ -0,0 +1,26 @@ +belongsTo(Organization::class); + } + + protected $casts = [ + 'languages' => 'array', + ]; + + protected $hidden = [ + 'created_at', + 'ext_id', + 'updated_at', + ]; + +} diff --git a/app/Models/Episode.php b/app/Models/Episode.php index b4a4ec8..ddd92a6 100644 --- a/app/Models/Episode.php +++ b/app/Models/Episode.php @@ -10,6 +10,10 @@ class Episode extends Model use HasFactory; + public function channels() { + return $this->belongsToMany(Channel::class); + } + public function event() { return $this->belongsTo(Event::class); } @@ -20,6 +24,13 @@ class Episode extends Model protected $casts = [ 'confirmed' => 'boolean', + 'start' => 'datetime', + ]; + + protected $hidden = [ + 'created_at', + 'ext_id', + 'updated_at', ]; } diff --git a/app/Models/EpisodePlayer.php b/app/Models/EpisodePlayer.php index 46a064b..9099190 100644 --- a/app/Models/EpisodePlayer.php +++ b/app/Models/EpisodePlayer.php @@ -19,6 +19,7 @@ class EpisodePlayer extends Model protected $hidden = [ 'created_at', + 'ext_id', 'updated_at', ]; diff --git a/app/Models/Event.php b/app/Models/Event.php index 44bc69b..b566dbb 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -15,7 +15,14 @@ class Event extends Model } protected $casts = [ + 'end' => 'datetime', + 'start' => 'datetime', 'visible' => 'boolean', ]; + protected $hidden = [ + 'created_at', + 'updated_at', + ]; + } diff --git a/app/Models/Organization.php b/app/Models/Organization.php new file mode 100644 index 0000000..61a9182 --- /dev/null +++ b/app/Models/Organization.php @@ -0,0 +1,16 @@ +hasMany(Channel::class); + } + +} diff --git a/database/migrations/2023_02_20_084120_create_organizations_table.php b/database/migrations/2023_02_20_084120_create_organizations_table.php new file mode 100644 index 0000000..ce00e8e --- /dev/null +++ b/database/migrations/2023_02_20_084120_create_organizations_table.php @@ -0,0 +1,39 @@ +id(); + $table->string('name')->unique(); + $table->text('title'); + $table->timestamps(); + }); + + $sg = new Organization(); + $sg->name = 'sg'; + $sg->title = 'SpeedGaming'; + $sg->save(); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('organizations'); + } +}; diff --git a/database/migrations/2023_02_20_084344_create_channels_table.php b/database/migrations/2023_02_20_084344_create_channels_table.php new file mode 100644 index 0000000..904d15c --- /dev/null +++ b/database/migrations/2023_02_20_084344_create_channels_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('organization_id')->nullable()->default(null)->constrained(); + $table->string('short_name'); + $table->text('title'); + $table->text('stream_link'); + $table->text('languages')->default('[]'); + $table->string('ext_id')->nullable()->default(null); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('channels'); + } +}; diff --git a/database/migrations/2023_02_20_093249_create_channel_episode_table.php b/database/migrations/2023_02_20_093249_create_channel_episode_table.php new file mode 100644 index 0000000..7f33734 --- /dev/null +++ b/database/migrations/2023_02_20_093249_create_channel_episode_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('channel_id')->constrained(); + $table->foreignId('episode_id')->constrained(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('channel_episode'); + } +}; diff --git a/resources/js/components/episodes/Channel.js b/resources/js/components/episodes/Channel.js new file mode 100644 index 0000000..1943753 --- /dev/null +++ b/resources/js/components/episodes/Channel.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button } from 'react-bootstrap'; + +import Icon from '../common/Icon'; + +const Channel = ({ channel }) => +
+ +
; + +Channel.propTypes = { + channel: PropTypes.shape({ + short_name: PropTypes.string, + stream_link: PropTypes.string, + title: PropTypes.string, + }), +}; + +export default Channel; diff --git a/resources/js/components/episodes/Channels.js b/resources/js/components/episodes/Channels.js new file mode 100644 index 0000000..fb10147 --- /dev/null +++ b/resources/js/components/episodes/Channels.js @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import Channel from './Channel'; + +const Channels = ({ channels }) => +
+ {channels.map(channel => + + )} +
; + +Channels.propTypes = { + channels: PropTypes.arrayOf(PropTypes.shape({ + })), +}; + +export default Channels; diff --git a/resources/js/components/episodes/Item.js b/resources/js/components/episodes/Item.js index 8f1bafc..3a3afa4 100644 --- a/resources/js/components/episodes/Item.js +++ b/resources/js/components/episodes/Item.js @@ -1,34 +1,70 @@ +import moment from 'moment'; import PropTypes from 'prop-types'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import Channels from './Channels'; import Players from './Players'; +const isActive = episode => { + if (!episode.start) return false; + const now = moment(); + const start = moment(episode.start); + const end = moment(start).add(episode.estimate, 'seconds'); + return start.isBefore(now) && end.isAfter(now); +}; + const Item = ({ episode }) => { const { t } = useTranslation(); - return
+ const classNames = [ + 'episodes-item', + 'd-flex', + 'align-items-start', + 'my-3', + 'p-2', + 'border', + 'rounded', + ]; + if (isActive(episode)) { + classNames.push('is-active'); + } + + return
{t('schedule.startTime', { date: new Date(episode.start) })}
- {episode.title ? -
- {episode.title} +
+
+ {episode.title ? +
+ {episode.title} +
+ : null} + {episode.event ? +
+ {episode.event.title} +
+ : null}
- : null} - {episode.event ? -
- {episode.event.title} +
+ {episode.channels ? + + : null}
+
+ {episode.players ? + : null} -
; }; Item.propTypes = { episode: PropTypes.shape({ + channels: PropTypes.arrayOf(PropTypes.shape({ + })), event: PropTypes.shape({ title: PropTypes.string, }), diff --git a/resources/sass/episodes.scss b/resources/sass/episodes.scss index 242699a..fb34fa5 100644 --- a/resources/sass/episodes.scss +++ b/resources/sass/episodes.scss @@ -1,4 +1,9 @@ .episodes-item { + &.is-active { + border-color: $success !important; + box-shadow: 0 0 0.25rem 0.25rem $success; + } + .episode-start { width: 4rem; } @@ -6,6 +11,7 @@ display: grid; grid-template-columns: 1fr 1fr; } + .player-link { border: none; -- 2.39.2