From 2104dded3691c78ee831f03b5791e9ac8653a083 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:18:26 +0200 Subject: [PATCH 01/32] Only parse categories when needed --- src/Domain/Round/IFSCRoundNameNormalizer.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Domain/Round/IFSCRoundNameNormalizer.php b/src/Domain/Round/IFSCRoundNameNormalizer.php index af6d712..9a0b87e 100644 --- a/src/Domain/Round/IFSCRoundNameNormalizer.php +++ b/src/Domain/Round/IFSCRoundNameNormalizer.php @@ -15,13 +15,11 @@ public function normalize(IFSCParsedTags $tags, string $originalName): string { $originalName = trim($originalName); - - $categories = $tags->getCategories(); $disciplines = $tags->getDisciplines(); $kind = $tags->getRoundKind(); if (!$tags->isPreRound() && $disciplines && $kind) { - $roundName = $this->buildCategories($categories); + $roundName = $this->buildCategories($tags); $roundName .= $this->buildDisciplines($disciplines); $roundName .= " {$kind->value}"; @@ -49,9 +47,10 @@ private function disciplineNames(array $disciplines): array return array_map(static fn (IFSCDiscipline $discipline): string => $discipline->value, $disciplines); } - /** @param IFSCRoundCategory[] $categories */ - private function buildCategories(array $categories): string + private function buildCategories(IFSCParsedTags $tags): string { + $categories = $tags->getCategories(); + if (count($categories) === 2) { return "Men's & Women's"; } else { From a458547b017a9acd40152b5d2f364fe57661a416 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:19:07 +0200 Subject: [PATCH 02/32] Remove redundant PHP action --- .github/workflows/push-docker-image.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/push-docker-image.yml b/.github/workflows/push-docker-image.yml index 53c0c11..a6ecbcf 100644 --- a/.github/workflows/push-docker-image.yml +++ b/.github/workflows/push-docker-image.yml @@ -13,11 +13,6 @@ jobs: - name: "Checkout code" uses: actions/checkout@master - - name: "Setup PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - - name: "Build and publish a Docker image for ${{ github.repository }}" uses: macbre/push-to-ghcr@master with: From b4f5fd04686dde67ab7f06ccba45c3e11a59f7dd Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:23:19 +0200 Subject: [PATCH 03/32] Rename classes and add tests --- config/services/domain.yml | 6 +-- src/Domain/Event/IFSCEventFactory.php | 2 +- .../{IFSCEventsSlug.php => IFSCEventSlug.php} | 2 +- ...rovider.php => InfoSheetRoundProvider.php} | 2 +- tests/unit/Domain/Event/IFSCEventSlugTest.php | 51 +++++++++++++++++++ 5 files changed, 57 insertions(+), 6 deletions(-) rename src/Domain/Event/{IFSCEventsSlug.php => IFSCEventSlug.php} (94%) rename src/Infrastructure/Round/{PDFRoundProvider.php => InfoSheetRoundProvider.php} (96%) create mode 100644 tests/unit/Domain/Event/IFSCEventSlugTest.php diff --git a/config/services/domain.yml b/config/services/domain.yml index 18e8c95..67ea8f7 100644 --- a/config/services/domain.yml +++ b/config/services/domain.yml @@ -86,10 +86,10 @@ services: class: nicoSWD\IfscCalendar\Domain\Event\IFSCEventFactory arguments: - '@nicoSWD\IfscCalendar\Domain\Calendar\SiteURLBuilder' - - '@nicoSWD\IfscCalendar\Domain\Event\IFSCEventsSlug' + - '@nicoSWD\IfscCalendar\Domain\Event\IFSCEventSlug' - '@nicoSWD\IfscCalendar\Domain\StartList\IFSCStartListGenerator' - nicoSWD\IfscCalendar\Domain\Event\IFSCEventsSlug: ~ + nicoSWD\IfscCalendar\Domain\Event\IFSCEventSlug: ~ nicoSWD\IfscCalendar\Domain\Event\IFSCEventSorter: ~ nicoSWD\IfscCalendar\Domain\Event\Helpers\DOMHelper: ~ @@ -98,7 +98,7 @@ services: class: nicoSWD\IfscCalendar\Domain\Round\IFSCRoundsScraper arguments: - '@nicoSWD\IfscCalendar\Domain\Round\IFSCRoundFactory' - - '@nicoSWD\IfscCalendar\Infrastructure\Round\PDFRoundProvider' + - '@nicoSWD\IfscCalendar\Infrastructure\Round\InfoSheetRoundProvider' nicoSWD\IfscCalendar\Domain\Round\IFSCRoundFactory: class: nicoSWD\IfscCalendar\Domain\Round\IFSCRoundFactory diff --git a/src/Domain/Event/IFSCEventFactory.php b/src/Domain/Event/IFSCEventFactory.php index 9b93217..e15afe7 100644 --- a/src/Domain/Event/IFSCEventFactory.php +++ b/src/Domain/Event/IFSCEventFactory.php @@ -22,7 +22,7 @@ { public function __construct( private SiteURLBuilder $siteURLBuilder, - private IFSCEventsSlug $eventsSlug, + private IFSCEventSlug $eventsSlug, private IFSCStartListGenerator $startListGenerator, ) { } diff --git a/src/Domain/Event/IFSCEventsSlug.php b/src/Domain/Event/IFSCEventSlug.php similarity index 94% rename from src/Domain/Event/IFSCEventsSlug.php rename to src/Domain/Event/IFSCEventSlug.php index 3abee01..7f46f14 100644 --- a/src/Domain/Event/IFSCEventsSlug.php +++ b/src/Domain/Event/IFSCEventSlug.php @@ -7,7 +7,7 @@ */ namespace nicoSWD\IfscCalendar\Domain\Event; -final readonly class IFSCEventsSlug +final readonly class IFSCEventSlug { public function create(string $eventName): string { diff --git a/src/Infrastructure/Round/PDFRoundProvider.php b/src/Infrastructure/Round/InfoSheetRoundProvider.php similarity index 96% rename from src/Infrastructure/Round/PDFRoundProvider.php rename to src/Infrastructure/Round/InfoSheetRoundProvider.php index a96a84d..aaecb09 100644 --- a/src/Infrastructure/Round/PDFRoundProvider.php +++ b/src/Infrastructure/Round/InfoSheetRoundProvider.php @@ -14,7 +14,7 @@ use nicoSWD\IfscCalendar\Infrastructure\Schedule\InfoSheetScheduleProvider; use Override; -final readonly class PDFRoundProvider implements IFSCRoundProviderInterface +final readonly class InfoSheetRoundProvider implements IFSCRoundProviderInterface { public function __construct( private InfoSheetScheduleProvider $scheduleProvider, diff --git a/tests/unit/Domain/Event/IFSCEventSlugTest.php b/tests/unit/Domain/Event/IFSCEventSlugTest.php new file mode 100644 index 0000000..f255682 --- /dev/null +++ b/tests/unit/Domain/Event/IFSCEventSlugTest.php @@ -0,0 +1,51 @@ + + */ +namespace nicoSWD\IfscCalendar\tests\Domain\Event; + +use nicoSWD\IfscCalendar\Domain\Event\IFSCEventSlug; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +final class IFSCEventSlugTest extends TestCase +{ + private IFSCEventSlug $slug; + + protected function setUp(): void + { + $this->slug = new IFSCEventSlug(); + } + + #[Test] + #[DataProvider('expected_slugs')] + public function season_2024_slugs(string $eventName, string $expectedSlug): void + { + $this->assertSame($expectedSlug, $this->slug->create($eventName)); + } + + public static function expected_slugs(): array + { + return [ + ['IFSC World Cup Keqiao 2024', 'ifsc-world-cup-keqiao-2024'], + ['IFSC World Cup Wujiang 2024', 'ifsc-world-cup-wujiang-2024'], + ['IFSC World Cup Salt Lake City 2024', 'ifsc-world-cup-salt-lake-city-2024'], + ['IFSC World Cup Innsbruck 2024', 'ifsc-world-cup-innsbruck-2024'], + ['IFSC World Cup Chamonix 2024', 'ifsc-world-cup-chamonix-2024'], + ['IFSC World Cup Briançon 2024', 'ifsc-world-cup-briancon-2024'], + ['IFSC World Cup Koper 2024', 'ifsc-world-cup-koper-2024'], + ['IFSC World Cup Prague 2024', 'ifsc-world-cup-prague-2024'], + ['IFSC World Cup Seoul 2024', 'ifsc-world-cup-seoul-2024'], + ['IFSC Paraclimbing World Cup Salt Lake City 2024', 'ifsc-paraclimbing-world-cup-salt-lake-city-2024'], + ['IFSC Paraclimbing World Cup Innsbruck 2024', 'ifsc-paraclimbing-world-cup-innsbruck-2024'], + ['IFSC Paraclimbing World Cup Arco 2024', 'ifsc-paraclimbing-world-cup-arco-2024'], + ['Olympic Qualifier Series Shanghai 2024', 'olympic-qualifier-series-shanghai-2024'], + ['Olympic Qualifier Series Budapest 2024', 'olympic-qualifier-series-budapest-2024'], + ['Olympic Games Paris 2024', 'olympic-games-paris-2024'], + ]; + } +} From bbf518724ace6756a34502ae111b6e62609a6019 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:24:23 +0200 Subject: [PATCH 04/32] Remove poster property as they're no longer a thing --- src/Domain/Event/IFSCEventsFetcher.php | 20 ++++++++++---------- src/Domain/Event/IFSCScrapedEventsResult.php | 1 - 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Domain/Event/IFSCEventsFetcher.php b/src/Domain/Event/IFSCEventsFetcher.php index 6f1ab17..cbaa93e 100644 --- a/src/Domain/Event/IFSCEventsFetcher.php +++ b/src/Domain/Event/IFSCEventsFetcher.php @@ -58,22 +58,13 @@ public function __construct( season: $season, event: $event, rounds: $rounds, - posterUrl: $scrapedRounds->posterUrl, + posterUrl: null, ); } return $events; } - /** - * @throws IFSCEventsScraperException - * @throws Exception - */ - private function fetchScrapedRounds(IFSCEventInfo $event): IFSCScrapedEventsResult - { - return $this->roundsScraper->fetchRoundsAndPosterForEvent($event); - } - /** @return IFSCRound[] */ private function generateRounds(IFSCEventInfo $event): array { @@ -127,6 +118,15 @@ private function fetchLeaguesForSeason(IFSCSeasonYear $season, array $selectedLe return $filteredLeagues; } + /** + * @throws IFSCEventsScraperException + * @throws Exception + */ + private function fetchScrapedRounds(IFSCEventInfo $event): IFSCScrapedEventsResult + { + return $this->roundsScraper->fetchRoundsForEvent($event); + } + /** * @param string[] $selectedLeagues * @return IFSCEventInfo[] diff --git a/src/Domain/Event/IFSCScrapedEventsResult.php b/src/Domain/Event/IFSCScrapedEventsResult.php index db96cc5..ba43927 100644 --- a/src/Domain/Event/IFSCScrapedEventsResult.php +++ b/src/Domain/Event/IFSCScrapedEventsResult.php @@ -13,7 +13,6 @@ { /** @param IFSCRound[] $rounds */ public function __construct( - public ?string $posterUrl, public array $rounds, ) { } From 0827108652f152358aa67035f3bce6d47b165c4f Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:25:44 +0200 Subject: [PATCH 05/32] Use YouTube video duration to calculate end time --- src/Domain/Round/IFSCRoundFactory.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Domain/Round/IFSCRoundFactory.php b/src/Domain/Round/IFSCRoundFactory.php index 6e93936..ca08283 100644 --- a/src/Domain/Round/IFSCRoundFactory.php +++ b/src/Domain/Round/IFSCRoundFactory.php @@ -35,7 +35,7 @@ public function create( if ($liveStream->scheduledStartTime) { $startTime = $this->buildStartTime($liveStream, $event); - $endTime = $startTime->modify('90 minutes'); + $endTime = $this->buildEndTime($startTime, $liveStream); $status = IFSCRoundStatus::CONFIRMED; } @@ -66,4 +66,17 @@ private function findLiveStream(IFSCEventInfo $event, string $roundName): LiveSt { return $this->liveStreamFinder->findLiveStream($event, $roundName); } + + private function buildEndTime(DateTimeImmutable $startTime, LiveStream $liveStream): DateTimeImmutable + { + if ($liveStream->duration > 0) { + $duration = $liveStream->duration; + } else { + $duration = 90; + } + + return $startTime->modify( + sprintf('+%d minutes', $duration) + ); + } } From 71a26885c83ed9d0b985f208a96f1af2a6e5df22 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:26:19 +0200 Subject: [PATCH 06/32] Cleanup --- src/Domain/Round/IFSCRoundsScraper.php | 31 ++++++++++---------------- src/Domain/Tags/IFSCTagsParser.php | 6 ++--- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/Domain/Round/IFSCRoundsScraper.php b/src/Domain/Round/IFSCRoundsScraper.php index 5a38c97..dc22aeb 100644 --- a/src/Domain/Round/IFSCRoundsScraper.php +++ b/src/Domain/Round/IFSCRoundsScraper.php @@ -10,7 +10,6 @@ use Exception; use nicoSWD\IfscCalendar\Domain\Event\IFSCScrapedEventsResult; use nicoSWD\IfscCalendar\Domain\Event\Info\IFSCEventInfo; -use nicoSWD\IfscCalendar\Domain\Schedule\IFSCSchedule; final readonly class IFSCRoundsScraper { @@ -21,34 +20,28 @@ public function __construct( } /** @throws Exception */ - public function fetchRoundsAndPosterForEvent(IFSCEventInfo $event): IFSCScrapedEventsResult + public function fetchRoundsForEvent(IFSCEventInfo $event): IFSCScrapedEventsResult { - $rounds = $this->roundProvider->fetchRounds($event); - return new IFSCScrapedEventsResult( - posterUrl: null, - rounds: $this->createRounds($event, $rounds), + rounds: $this->createRounds($event), ); } - /** - * @param IFSCSchedule[] $schedules - * @return IFSCRound[] - */ - private function createRounds(IFSCEventInfo $event, array $schedules): array + /** @return IFSCRound[] */ + private function createRounds(IFSCEventInfo $event): array { - $rounds = []; + $schedules = []; - foreach ($schedules as $schedule) { - $rounds[] = $this->roundFactory->create( + foreach ($this->roundProvider->fetchRounds($event) as $round) { + $schedules[] = $this->roundFactory->create( event: $event, - roundName: $schedule->name, - startTime: $schedule->startsAt, - endTime: $schedule->endsAt, - status: IFSCRoundStatus::CONFIRMED, + roundName: $round->name, + startTime: $round->startsAt, + endTime: $round->endsAt, + status: IFSCRoundStatus::PROVISIONAL, ); } - return $rounds; + return $schedules; } } diff --git a/src/Domain/Tags/IFSCTagsParser.php b/src/Domain/Tags/IFSCTagsParser.php index 507fe61..c68c4ca 100644 --- a/src/Domain/Tags/IFSCTagsParser.php +++ b/src/Domain/Tags/IFSCTagsParser.php @@ -15,9 +15,9 @@ public function fromString(string $string): IFSCParsedTags { $tags = []; - foreach (Tag::cases() as $eventType) { - if (preg_match("~\b{$eventType->value}\b~i", strtolower($string))) { - $tags[] = $eventType; + foreach (Tag::cases() as $tag) { + if (preg_match("~\b{$tag->value}\b~i", $string)) { + $tags[] = $tag; } } From be140ba19771bf42b18a76f8964607cfdda70cef Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:26:58 +0200 Subject: [PATCH 07/32] Add YouTube video duration --- src/Domain/Stream/LiveStream.php | 1 + src/Domain/YouTube/YouTubeLinkMatcher.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Domain/Stream/LiveStream.php b/src/Domain/Stream/LiveStream.php index 4379617..119f07e 100644 --- a/src/Domain/Stream/LiveStream.php +++ b/src/Domain/Stream/LiveStream.php @@ -15,6 +15,7 @@ public function __construct( public ?string $url = null, public ?DateTimeImmutable $scheduledStartTime = null, + public int $duration = 0, public array $restrictedRegions = [], ) { } diff --git a/src/Domain/YouTube/YouTubeLinkMatcher.php b/src/Domain/YouTube/YouTubeLinkMatcher.php index ea6adb5..92cef4e 100644 --- a/src/Domain/YouTube/YouTubeLinkMatcher.php +++ b/src/Domain/YouTube/YouTubeLinkMatcher.php @@ -30,6 +30,7 @@ public function findStreamUrlForRound(IFSCEventInfo $event, string $roundName, Y return new LiveStream( url: self::YOUTUBE_BASE_URL . $video->videoId, scheduledStartTime: $video->scheduledStartTime, + duration: $video->duration, restrictedRegions: $video->restrictedRegions, ); } From 96d3d8e006b7e40b6c88275c38f2ef38dfefeeb9 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:27:28 +0200 Subject: [PATCH 08/32] Add provisional status --- src/Domain/Round/IFSCRoundStatus.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Domain/Round/IFSCRoundStatus.php b/src/Domain/Round/IFSCRoundStatus.php index 4051ebe..be0afce 100644 --- a/src/Domain/Round/IFSCRoundStatus.php +++ b/src/Domain/Round/IFSCRoundStatus.php @@ -11,6 +11,7 @@ enum IFSCRoundStatus: string { case CONFIRMED = 'confirmed'; case ESTIMATED = 'estimated'; + case PROVISIONAL = 'provisional'; public function isConfirmed(): bool { From 85563ae9be54f6c89a3cd73a3caa3b38fa11c11b Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:27:50 +0200 Subject: [PATCH 09/32] Add tests --- .../Domain/Round/IFSCRoundNormalizerTest.php | 122 +++++++++++++++++ .../Schedule/IFSCScheduleFactoryTest.php | 128 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 tests/unit/Domain/Round/IFSCRoundNormalizerTest.php create mode 100644 tests/unit/Domain/Schedule/IFSCScheduleFactoryTest.php diff --git a/tests/unit/Domain/Round/IFSCRoundNormalizerTest.php b/tests/unit/Domain/Round/IFSCRoundNormalizerTest.php new file mode 100644 index 0000000..b33097d --- /dev/null +++ b/tests/unit/Domain/Round/IFSCRoundNormalizerTest.php @@ -0,0 +1,122 @@ + + */ +namespace nicoSWD\IfscCalendar\tests\Domain\Round; + +use nicoSWD\IfscCalendar\Domain\Round\IFSCRoundNameNormalizer; +use nicoSWD\IfscCalendar\Domain\Tags\IFSCTagsParser; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +final class IFSCRoundNormalizerTest extends TestCase +{ + private IFSCRoundNameNormalizer $normalizer; + private IFSCTagsParser $tagsParser; + + protected function setUp(): void + { + $this->normalizer = new IFSCRoundNameNormalizer(); + $this->tagsParser = new IFSCTagsParser(); + } + + #[Test] + #[DataProvider('event_names')] + public function event_names_normalize(string $eventName, string $expectedName): void + { + $tags = $this->tagsParser->fromString($eventName); + + $this->assertSame($expectedName, $this->normalizer->normalize($tags, $eventName)); + } + + public static function event_names(): array + { + return [ + ["Opening of Isolation", "Opening Of Isolation"], + ["Women’s Boulder Qualification", "Women's Boulder Qualification"], + ["Men’s Boulder Qualification", "Men's Boulder Qualification"], + ["Women’s Boulder Semi final", "Women's Boulder Semi-Final"], + ["Women’s Boulder final", "Women's Boulder Final"], + ["Men’s Boulder Semi final", "Men's Boulder Semi-Final"], + ["Men’s Boulder Final", "Men's Boulder Final"], + ["Lead Warm Up Zone Open", "Lead Warm Up Zone Open"], + ["Lead Qualification", "Men's & Women's Lead Qualification"], + ["Speed Warm Up Zone Open", "Speed Warm Up Zone Open"], + ["Speed Practice", "Speed Practice"], + ["Speed Qualification", "Men's & Women's Speed Qualification"], + ["Isolation Opens", "Isolation Opens"], + ["Isolation Closes", "Isolation Closes"], + ["Men’s & Women’s Lead Semi Finals", "Men's & Women's Lead Semi-Final"], + ["Men’s & Women’s Speed - Finals", "Men's & Women's Speed Final"], + ["Women’s Lead - Final", "Women's Lead Final"], + ["Men’s Lead - Final", "Men's Lead Final"], + ["Speed Training", "Speed Training"], + ["Men’s Boulder qualification", "Men's Boulder Qualification"], + ["Women’s Boulder isolation zone opens", "Women’s Boulder Isolation Zone Opens"], + ["Women’s Boulder qualification", "Women's Boulder Qualification"], + ["Men’s Boulder Semifinals", "Men's Boulder Semi-Final"], + ["Women’s Speed warm-up", "Women’s Speed Warm-Up"], + ["Women’s Speed practice", "Women’s Speed Practice"], + ["Women’s Speed Qualification", "Women's Speed Qualification"], + ["Men’s Boulder final", "Men's Boulder Final"], + ["Women’s Speed Final", "Women's Speed Final"], + ["Women’s Boulder Semifinals", "Women's Boulder Semi-Final"], + ["Men’s Speed warm-up", "Men’s Speed Warm-Up"], + ["Men’s Speed practice", "Men’s Speed Practice"], + ["Men’s Speed Qualification", "Men's Speed Qualification"], + ["Women’s Boulder final", "Women's Boulder Final"], + ["Men’s Speed Final", "Men's Speed Final"], + ["Women’s Boulder Semi-Final", "Women's Boulder Semi-Final"], + ["Women’s Boulder Final", "Women's Boulder Final"], + ["Men’s Boulder Semi-Final", "Men's Boulder Semi-Final"], + ["Men’s Boulder Final", "Men's Boulder Final"], + ["Warm-Up Lead Qualification opens", "Warm-Up Lead Qualification Opens"], + ["Men’s & Women’s Lead Qualification", "Men's & Women's Lead Qualification"], + ["Observation Lead Semi-Final", "Observation Lead Semi-Final"], + ["Men’s & Women’s Lead Semi-Finals ", "Men's & Women's Lead Semi-Final"], + ["Presentation & Observation Lead Final", "Presentation & Observation Lead Final"], + ["Men’s Lead Final", "Men's Lead Final"], + ["Women’s Lead Final", "Women's Lead Final"], + ["Registration IFSC World Cup SPEED", "Registration IFSC World Cup SPEED"], + ["Speed Practice Women then Men", "Speed Practice Women Then Men"], + ["Speed Qualification Women then Men", "Men's & Women's Speed Qualification"], + ["Registration IFSC World Cup LEAD", "Registration IFSC World Cup LEAD"], + ["Technical Meeting LEAD", "Technical Meeting LEAD"], + ["Qualification men & women LEAD", "Men's & Women's Lead Qualification"], + ["Speed Finals women and men", "Men's & Women's Speed Final"], + ["Lead semi-finals isolation Zone opens", "Lead Semi-Finals Isolation Zone Opens"], + ["Lead semi-finals isolation closes", "Lead Semi-Finals Isolation Closes"], + ["Men’s and women’s LEAD semi-finals", "Men's & Women's Lead Semi-Final"], + ["Lead Final isolation zone opens", "Lead Final Isolation Zone Opens"], + ["Observation both genders at the same time", "Observation Both Genders At The Same Time"], + ["Lead Women Final", "Women's Lead Final"], + ["Lead Men Final", "Men's Lead Final"], + ["Speed Qualification Women then Men", "Men's & Women's Speed Qualification"], + ["Start qualification men & women", "Start Qualification Men & Women"], + ["Semi-finals men and women", "Semi-Finals Men And Women"], + ["Presentation, then men final", "Presentation, Then Men Final"], + ["Presentation, then women final", "Presentation, Then Women Final"], + ["Registration IFSC World Cup Lead (competition venue)", "Registration IFSC World Cup Lead (competition Venue)"], + ["Technical Meeting LEAD (competition venue)", "Technical Meeting LEAD (competition Venue)"], + ["Women’s and Men’s Lead Qualifications", "Men's & Women's Lead Qualification"], + ["Women’s and Men’s Lead semi-finals isolation Zone", "Women’s And Men’s Lead Semi-Finals Isolation Zone"], + ["Observation Women", "Observation Women"], + ["Observation Men", "Observation Men"], + ["Women’s and Men’s Lead semi-finals", "Men's & Women's Lead Semi-Final"], + ["Observation time Men and Women", "Observation Time Men And Women"], + ["Men’s Lead Final", "Men's Lead Final"], + ["Men’s Boulder final", "Men's Boulder Final"], + ["Women’s Boulder Final", "Women's Boulder Final"], + ["Technical Meeting Paraclimbing", "Technical Meeting Paraclimbing"], + ["Paraclimbing Finals", "Paraclimbing Finals"], + ["Warm-Up Paraclimbing Opens", "Warm-Up Paraclimbing Opens"], + ["Paraclimbing Qualification", "Paraclimbing Qualification"], + ["Isolation Zone open / close Paraclimbing Finals", "Isolation Zone Open / Close Paraclimbing Finals"], + ["Paraclimbing Finals", "Paraclimbing Finals"], + ]; + } +} diff --git a/tests/unit/Domain/Schedule/IFSCScheduleFactoryTest.php b/tests/unit/Domain/Schedule/IFSCScheduleFactoryTest.php new file mode 100644 index 0000000..4593989 --- /dev/null +++ b/tests/unit/Domain/Schedule/IFSCScheduleFactoryTest.php @@ -0,0 +1,128 @@ + + */ +namespace nicoSWD\IfscCalendar\tests\Domain\Schedule; + +use DateTimeImmutable; +use nicoSWD\IfscCalendar\Domain\Round\IFSCRoundNameNormalizer; +use nicoSWD\IfscCalendar\Domain\Schedule\IFSCScheduleFactory; +use nicoSWD\IfscCalendar\Domain\Tags\IFSCTagsParser; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +final class IFSCScheduleFactoryTest extends TestCase +{ + private IFSCScheduleFactory $scheduleFactory; + + protected function setUp(): void + { + $this->scheduleFactory = new IFSCScheduleFactory( + new IFSCTagsParser(), + new IFSCRoundNameNormalizer(), + ); + } + + #[Test] + #[DataProvider('event_names')] + public function event_names_normalize(string $eventName, bool $isPreRound): void + { + $schedule = $this->scheduleFactory->create( + name: $eventName, + startsAt: new DateTimeImmutable(), + endsAt: new DateTimeImmutable(), + ); + + $this->assertSame($schedule->isPreRound, $isPreRound); + } + + public static function event_names(): array + { + return [ + ["Women’s Boulder Qualification", false], + ["Men’s Boulder Qualification", false], + ["Women’s Boulder Semi final", false], + ["Women’s Boulder final", false], + ["Men’s Boulder Semi final", false], + ["Men’s Boulder Final", false], + ["Lead Warm Up Zone Open", true], + ["Lead Qualification", false], + ["Speed Warm Up Zone Open", true], + ["Speed Practice", true], + ["Speed Qualification", false], + ["Isolation Opens", true], + ["Isolation Closes", true], + ["Men’s & Women’s Lead Semi Finals", false], + ["Men’s & Women’s Speed - Finals", false], + ["Women’s Lead - Final", false], + ["Men’s Lead - Final", false], + ["Speed Training", true], + ["Men’s Boulder qualification", false], + ["Women’s Boulder isolation zone opens", true], + ["Women’s Boulder qualification", false], + ["Men’s Boulder Semifinals", false], + ["Women’s Speed warm-up", true], + ["Women’s Speed practice", true], + ["Women’s Speed Qualification", false], + ["Men’s Boulder final", false], + ["Women’s Speed Final", false], + ["Women’s Boulder Semifinals", false], + ["Men’s Speed warm-up", true], + ["Men’s Speed practice", true], + ["Men’s Speed Qualification", false], + ["Women’s Boulder final", false], + ["Men’s Speed Final", false], + ["Women’s Boulder Semi-Final", false], + ["Women’s Boulder Final", false], + ["Men’s Boulder Semi-Final", false], + ["Men’s Boulder Final", false], + ["Warm-Up Lead Qualification opens", true], + ["Men’s & Women’s Lead Qualification", false], + ["Observation Lead Semi-Final", true], + ["Men’s & Women’s Lead Semi-Finals ", false], + ["Presentation & Observation Lead Final", true], + ["Men’s Lead Final", false], + ["Women’s Lead Final", false], + ["Registration IFSC World Cup SPEED", true], + ["Speed Practice Women then Men", true], + ["Speed Qualification Women then Men", false], + ["Registration IFSC World Cup LEAD", true], + ["Technical Meeting LEAD", true], + ["Qualification men & women LEAD", false], + ["Speed Finals women and men", false], + ["Lead semi-finals isolation Zone opens", true], + ["Lead semi-finals isolation closes", true], + ["Men’s and women’s LEAD semi-finals", false], + ["Lead Final isolation zone opens", true], + ["Observation both genders at the same time", true], + ["Lead Women Final", false], + ["Lead Men Final", false], + ["Speed Qualification Women then Men", false], + ["Start qualification men & women", true], + ["Semi-finals men and women", true], + ["Presentation, then men final", true], + ["Presentation, then women final", true], + ["Registration IFSC World Cup Lead (competition venue)", true], + ["Technical Meeting LEAD (competition venue)", true], + ["Women’s and Men’s Lead Qualifications", false], + ["Women’s and Men’s Lead semi-finals isolation Zone", true], + ["Observation Women", true], + ["Observation Men", true], + ["Women’s and Men’s Lead semi-finals", false], + ["Observation time Men and Women", true], + ["Men’s Lead Final", false], + ["Men’s Boulder final", false], + ["Women’s Boulder Final", false], + ["Technical Meeting Paraclimbing", true], + ["Paraclimbing Finals", true], + ["Warm-Up Paraclimbing Opens", true], + ["Paraclimbing Qualification", true], + ["Isolation Zone open / close Paraclimbing Finals", true], + ["Paraclimbing Finals", true], + ]; + } +} From 4b440593fe4069274e67222622a7c0b9019fbc57 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:28:13 +0200 Subject: [PATCH 10/32] Rename class --- config/services/infrastructure.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/services/infrastructure.yml b/config/services/infrastructure.yml index 76f59f9..9706b8e 100644 --- a/config/services/infrastructure.yml +++ b/config/services/infrastructure.yml @@ -56,8 +56,8 @@ services: - '@nicoSWD\IfscCalendar\Infrastructure\HttpClient\HttpGuzzleClient' - '@nicoSWD\IfscCalendar\Infrastructure\DomainEvent\SymfonyEventDispatcher' - nicoSWD\IfscCalendar\Infrastructure\Round\PDFRoundProvider: - class: nicoSWD\IfscCalendar\Infrastructure\Round\PDFRoundProvider + nicoSWD\IfscCalendar\Infrastructure\Round\InfoSheetRoundProvider: + class: nicoSWD\IfscCalendar\Infrastructure\Round\InfoSheetRoundProvider arguments: - '@nicoSWD\IfscCalendar\Infrastructure\Schedule\InfoSheetScheduleProvider' - '@nicoSWD\IfscCalendar\Infrastructure\Schedule\InfoSheetDownloader' From 11212b0ca04ca337eb6a79b9fffc976c2fab98d3 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Wed, 10 Apr 2024 22:30:03 +0200 Subject: [PATCH 11/32] Add source dir --- phpunit.xml.dist | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f91022f..f838f50 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,21 +1,20 @@ - - - - - - - - - - - - tests - - + + + + + + + + + + tests + + + + + src + + From c460752bb242865cce1dfa2ebfd3091b9d1c68a2 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 14:55:11 +0200 Subject: [PATCH 12/32] Add calendar diff to PR --- .github/workflows/pull_requests.yml | 40 ++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index b407ad7..cf6067f 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -5,7 +5,7 @@ on: branches: [ "main" ] jobs: - phpunit: + code-quality: runs-on: ubuntu-latest steps: - name: "Checkout code" @@ -24,3 +24,41 @@ jobs: uses: php-actions/phpstan@v3 with: level: 6 + + calendar-diff: + runs-on: ubuntu-latest + steps: + - name: "Checkout code" + uses: actions/checkout@master + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer + + - name: "Install poppler-utils" + run: apt update && apt install poppler-utils -y + + - name: "Build Calendar" + run: | + composer install --no-dev + php app/run.php --season 2024 --output new-calendar.json --format json + + - name: "Check diff" + run: | + curl -sSL "https://calendar.ifsc.stream/?format=json&nocache=1" --output old-calendar.json + php bin/calendar-diff old-calendar.json new-calendar.json > diff.md + + if [ $(wc -w < diff.md) -gt 0 ]; then + echo "This PR results in the following results in the `JSON` calendar" > calendar.diff + cat diff.md >> calendar.diff + else + echo "No changes in the `JSON`calendar" > calendar.diff + fi + + - name: "Comment calendar diff in PR" + uses: thollander/actions-comment-pull-request@v2 + with: + filePath: calendar.diff + comment_tag: calendar_diff From a5b01084798d22257bcb331bb406cc617633160c Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 14:56:44 +0200 Subject: [PATCH 13/32] Add calendar diff to PR --- .github/workflows/pull_requests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index cf6067f..333c6af 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -38,7 +38,7 @@ jobs: tools: composer - name: "Install poppler-utils" - run: apt update && apt install poppler-utils -y + run: sudo apt-get update && sudo apt-get install poppler-utils -y - name: "Build Calendar" run: | From 3b09bda0ef6ef05418e05bdb88ec5fc0a28d4d8b Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 14:59:26 +0200 Subject: [PATCH 14/32] Add calendar diff to PR --- .github/workflows/pull_requests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index 333c6af..74c50f6 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -4,6 +4,9 @@ on: pull_request: branches: [ "main" ] +permissions: + pull-requests: write + jobs: code-quality: runs-on: ubuntu-latest From b034a8ad284ab77f7f42b9a00965a1f78e49d282 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 15:03:42 +0200 Subject: [PATCH 15/32] Fix event start date --- src/Domain/Event/IFSCEventFactory.php | 2 +- src/Domain/Round/IFSCRoundStatus.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Domain/Event/IFSCEventFactory.php b/src/Domain/Event/IFSCEventFactory.php index e15afe7..34cc172 100644 --- a/src/Domain/Event/IFSCEventFactory.php +++ b/src/Domain/Event/IFSCEventFactory.php @@ -60,7 +60,7 @@ private function generateDateRangeFromRounds(array $rounds, IFSCEventInfo $event $confirmedDates = []; foreach ($rounds as $round) { - if ($round->status->isConfirmed()) { + if ($round->status->isConfirmed() || $round->status->isProvisional()) { $confirmedDates[] = $round->startTime; } } diff --git a/src/Domain/Round/IFSCRoundStatus.php b/src/Domain/Round/IFSCRoundStatus.php index be0afce..86ceebd 100644 --- a/src/Domain/Round/IFSCRoundStatus.php +++ b/src/Domain/Round/IFSCRoundStatus.php @@ -17,4 +17,9 @@ public function isConfirmed(): bool { return $this === self::CONFIRMED; } + + public function isProvisional(): bool + { + return $this === self::PROVISIONAL; + } } From e97f789fd393467dea9f12f3b630a2a3f2cc0693 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 15:04:09 +0200 Subject: [PATCH 16/32] Add 5 minutes to YouTube scheduled start time --- src/Domain/Round/IFSCRoundFactory.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Domain/Round/IFSCRoundFactory.php b/src/Domain/Round/IFSCRoundFactory.php index ca08283..799560b 100644 --- a/src/Domain/Round/IFSCRoundFactory.php +++ b/src/Domain/Round/IFSCRoundFactory.php @@ -58,7 +58,12 @@ private function getTags(string $string): IFSCParsedTags private function buildStartTime(LiveStream $liveStream, IFSCEventInfo $event): DateTimeImmutable { - return (new DateTimeImmutable($liveStream->scheduledStartTime->format(DateTimeInterface::RFC3339))) + $schedulesStartTime = $liveStream->scheduledStartTime->format( + DateTimeInterface::RFC3339, + ); + + return (new DateTimeImmutable($schedulesStartTime)) + ->modify('+5 minutes') ->setTimezone($event->timeZone); } @@ -75,7 +80,9 @@ private function buildEndTime(DateTimeImmutable $startTime, LiveStream $liveStre $duration = 90; } - return $startTime->modify( + $endTime = clone $startTime; + + return $endTime->modify( sprintf('+%d minutes', $duration) ); } From 35e499dc4191c1e64708f5a3a95cb7da1c756461 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 15:04:26 +0200 Subject: [PATCH 17/32] Cleanup --- src/Domain/Round/IFSCRoundsScraper.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Domain/Round/IFSCRoundsScraper.php b/src/Domain/Round/IFSCRoundsScraper.php index dc22aeb..9375036 100644 --- a/src/Domain/Round/IFSCRoundsScraper.php +++ b/src/Domain/Round/IFSCRoundsScraper.php @@ -30,10 +30,10 @@ public function fetchRoundsForEvent(IFSCEventInfo $event): IFSCScrapedEventsResu /** @return IFSCRound[] */ private function createRounds(IFSCEventInfo $event): array { - $schedules = []; + $rounds = []; foreach ($this->roundProvider->fetchRounds($event) as $round) { - $schedules[] = $this->roundFactory->create( + $rounds[] = $this->roundFactory->create( event: $event, roundName: $round->name, startTime: $round->startsAt, @@ -42,6 +42,6 @@ private function createRounds(IFSCEventInfo $event): array ); } - return $schedules; + return $rounds; } } From b94ba260dba89c653fbad9679797cf396fe92645 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 15:04:43 +0200 Subject: [PATCH 18/32] Add scrutinizer config --- .scrutinizer.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .scrutinizer.yml diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..0bf0136 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,16 @@ +build: + environment: + php: + version: 8.3.3 + nodes: + analysis: + tests: + override: + - php-scrutinizer-run +filter: + excluded_paths: + - tests/* + - vendor/* + +checks: + php: true From 469d6a188b2b19d1f8276cd3fcd5b1d7e541d2b4 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 15:10:40 +0200 Subject: [PATCH 19/32] Fix spelling --- .github/workflows/pull_requests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index 74c50f6..a3e5544 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -54,7 +54,7 @@ jobs: php bin/calendar-diff old-calendar.json new-calendar.json > diff.md if [ $(wc -w < diff.md) -gt 0 ]; then - echo "This PR results in the following results in the `JSON` calendar" > calendar.diff + echo "This PR results in the following changes in the `JSON` calendar" > calendar.diff cat diff.md >> calendar.diff else echo "No changes in the `JSON`calendar" > calendar.diff From 69bddcfc56b18dac22d20d2cd18d74cda13de645 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 19:08:02 +0200 Subject: [PATCH 20/32] Improve diff --- bin/calendar-diff | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/calendar-diff b/bin/calendar-diff index b9aefae..e3ff748 100755 --- a/bin/calendar-diff +++ b/bin/calendar-diff @@ -96,20 +96,18 @@ function generate_round_diff(array $oldRounds, array $newRounds): string $oldValue = $oldRound[$key]; if ($newValue !== $oldValue) { - $roundDiff .= "|{$key} |" . normalize_value($oldValue) . "|" . normalize_value($newValue) . "|" . PHP_EOL; + $roundDiff .= "| |{$key} |" . normalize_value($oldValue) . "|" . normalize_value($newValue) . "|" . PHP_EOL; } } else { if (normalize_value($newValue) !== 'null') { - $roundDiff .= "|{$key} |null|" . normalize_value($newValue) . "|" . PHP_EOL; + $roundDiff .= "| |{$key} |null|" . normalize_value($newValue) . "|" . PHP_EOL; } } } if ($roundDiff) { - $changedRounds .= "| {$newRound['name']} |--- |--- |" . PHP_EOL; - $changedRounds .= "|-----------------------|---------------|-----------------|" . PHP_EOL; - $changedRounds .= "| **Key** | **Old Value** | **New Value** |" . PHP_EOL; - $changedRounds .= $roundDiff . PHP_EOL; + $changedRounds .= "| **🟣 {$newRound['name']}** | | | |" . PHP_EOL; + $changedRounds .= $roundDiff; } } } @@ -117,7 +115,9 @@ function generate_round_diff(array $oldRounds, array $newRounds): string if ($changedRounds) { $diff .= PHP_EOL; $diff .= "#### Changed Rounds" . PHP_EOL; - $diff .= $changedRounds; + $diff .= "|Round | **Key** | **Old Value** | **New Value** |" . PHP_EOL; + $diff .= "|------|-----------------------|---------------|-----------------|" . PHP_EOL; + $diff .= $changedRounds . PHP_EOL; } if ($addedRounds) { @@ -168,7 +168,7 @@ foreach ($newEvents as $newEvent) { if (count($newEvent['start_list']) > 0) { $addedEvents .= "#### 📋 Start List:" . PHP_EOL; $addedEvents .= "|" . implode('|', array_keys($newEvent['start_list'][0])) . "|" . PHP_EOL; - $addedEvents .= "|" . str_repeat('-------------|', count($newEvent['start_list'][0])) . PHP_EOL; + $addedEvents .= "|" . str_repeat(' |', count($newEvent['start_list'][0])) . PHP_EOL; foreach ($newEvent['start_list'] as $startList) { foreach ($startList as $value) { @@ -250,7 +250,7 @@ foreach ($newEvents as $newEvent) { } if ($startListDiff) { - $changedEvents .= "| 📋 Start List | --- | --- | --- | --- |" . PHP_EOL; + $changedEvents .= "| 📋 Start List | | | | |" . PHP_EOL; $changedEvents .= "|---------------|-----------------|---------------|--------------------|-------------------|" . PHP_EOL; $changedEvents .= "| **Status** | **First Name** | **Last Name** | **New First Name** | **New Last Name** |" . PHP_EOL; $changedEvents .= $startListDiff . PHP_EOL; From f18d6d2334f0ff69600ec4272214662cfbaf46e9 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 20:42:48 +0200 Subject: [PATCH 21/32] Improve diff --- bin/calendar-diff | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/calendar-diff b/bin/calendar-diff index e3ff748..465633a 100755 --- a/bin/calendar-diff +++ b/bin/calendar-diff @@ -96,17 +96,17 @@ function generate_round_diff(array $oldRounds, array $newRounds): string $oldValue = $oldRound[$key]; if ($newValue !== $oldValue) { - $roundDiff .= "| |{$key} |" . normalize_value($oldValue) . "|" . normalize_value($newValue) . "|" . PHP_EOL; + $roundDiff .= "| 👉 |_{$key}_ |" . normalize_value($oldValue) . "|" . normalize_value($newValue) . "|" . PHP_EOL; } } else { if (normalize_value($newValue) !== 'null') { - $roundDiff .= "| |{$key} |null|" . normalize_value($newValue) . "|" . PHP_EOL; + $roundDiff .= "| 👉 |_{$key}_ |null|" . normalize_value($newValue) . "|" . PHP_EOL; } } } if ($roundDiff) { - $changedRounds .= "| **🟣 {$newRound['name']}** | | | |" . PHP_EOL; + $changedRounds .= "| **{$newRound['name']}** | | | |" . PHP_EOL; $changedRounds .= $roundDiff; } } From beac99d76744f8fa9b775a53e6e575a7acdae88a Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Thu, 11 Apr 2024 21:56:43 +0200 Subject: [PATCH 22/32] Remove redundant clone --- src/Domain/Round/IFSCRoundFactory.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Domain/Round/IFSCRoundFactory.php b/src/Domain/Round/IFSCRoundFactory.php index 799560b..63d43bd 100644 --- a/src/Domain/Round/IFSCRoundFactory.php +++ b/src/Domain/Round/IFSCRoundFactory.php @@ -17,6 +17,8 @@ final readonly class IFSCRoundFactory { + private const int DEFAULT_ROUND_DURATION = 90; + public function __construct( private IFSCTagsParser $tagsParser, private YouTubeLiveStreamFinder $liveStreamFinder, @@ -77,12 +79,10 @@ private function buildEndTime(DateTimeImmutable $startTime, LiveStream $liveStre if ($liveStream->duration > 0) { $duration = $liveStream->duration; } else { - $duration = 90; + $duration = self::DEFAULT_ROUND_DURATION; } - $endTime = clone $startTime; - - return $endTime->modify( + return $startTime->modify( sprintf('+%d minutes', $duration) ); } From 24196311aaf9b7c8ef9fef4d5700c816a69348fa Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Fri, 12 Apr 2024 18:09:23 +0200 Subject: [PATCH 23/32] Fix alarms, add speed qualifications to iCal calendar --- .github/workflows/pull_requests.yml | 4 +- README.md | 13 ++--- src/Domain/Discipline/IFSCDisciplines.php | 33 ++++++++++++ src/Domain/Event/IFSCEvent.php | 4 +- src/Domain/Event/IFSCEventFactory.php | 2 +- src/Domain/Event/IFSCEventsFetcher.php | 2 +- src/Domain/Round/IFSCRound.php | 11 ++-- src/Domain/Round/IFSCRoundFactory.php | 10 +++- src/Domain/Stream/LiveStream.php | 5 ++ .../YouTubeLiveStreamFinderFactory.php | 2 +- src/Infrastructure/Calendar/ICalCalendar.php | 52 ++++++++++++------- src/Infrastructure/Calendar/JsonCalendar.php | 8 +-- 12 files changed, 101 insertions(+), 45 deletions(-) create mode 100644 src/Domain/Discipline/IFSCDisciplines.php diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index a3e5544..488db02 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -54,10 +54,10 @@ jobs: php bin/calendar-diff old-calendar.json new-calendar.json > diff.md if [ $(wc -w < diff.md) -gt 0 ]; then - echo "This PR results in the following changes in the `JSON` calendar" > calendar.diff + echo "This PR produces the following changes in the calendar" > calendar.diff cat diff.md >> calendar.diff else - echo "No changes in the `JSON`calendar" > calendar.diff + echo "No changes in the calendar were produced" > calendar.diff fi - name: "Comment calendar diff in PR" diff --git a/README.md b/README.md index dc14fce..7b22032 100644 --- a/README.md +++ b/README.md @@ -129,15 +129,15 @@ $ ./build/ifsc-calendar.phar \ ## 🔧 Todo - [ ] Use Symfony serializer to build response object from IFSC API -- [ ] Calculate average event duration and add it to `events.json` and to the calendar as the official web - does not tell when an event finishes. +- [ ] Calculate average event duration (based on past events) +- [ ] Add proper start lists to rounds +- [ ] Add tickets URL? - [ ] Finish writing calendar setup guides - [ ] Cleanup PHP code - [ ] Add more tests -- [ ] Make scraping more robust and fail on errors or missing data -- [ ] Fix scraper for older seasons (formatting changes drastically) -- [ ] Add more domain events to improve output log -- [ ] Add automated tests to PRs (unit tests, coverage, etc) +- [x] Make scraping more robust and fail on errors or missing data +- [x] Add more domain events to improve output log +- [x] Add automated tests to PRs (unit tests, coverage, etc) - [x] Push Docker image to Docker Hub - [x] Add BuyMeACoffee link to `.ics` calendar events - [x] Show activity and warnings in console (domain events) @@ -151,6 +151,7 @@ $ ./build/ifsc-calendar.phar \ - [x] Always serve asset from latest release on calendar URL - [x] Fetch stream links from YouTube API if none can be scraped - [x] Automatically regenerate calendar and update release +- [ ] ~~Fix scraper for older seasons (formatting changes drastically)~~ ## Requirements - PHP 8.3 diff --git a/src/Domain/Discipline/IFSCDisciplines.php b/src/Domain/Discipline/IFSCDisciplines.php new file mode 100644 index 0000000..4a52d72 --- /dev/null +++ b/src/Domain/Discipline/IFSCDisciplines.php @@ -0,0 +1,33 @@ + + */ +namespace nicoSWD\IfscCalendar\Domain\Discipline; + +final readonly class IFSCDisciplines +{ + /** @param IFSCDiscipline[] $disciplines */ + public function __construct( + private array $disciplines, + ) { + } + + public function isSpeed(): bool + { + return $this->hasDiscipline(IFSCDiscipline::SPEED); + } + + /** @return IFSCDiscipline[] */ + public function all(): array + { + return $this->disciplines; + } + + public function hasDiscipline(IFSCDiscipline $discipline): bool + { + return in_array($discipline, $this->disciplines, strict: true); + } +} diff --git a/src/Domain/Event/IFSCEvent.php b/src/Domain/Event/IFSCEvent.php index 0e71127..45db0cf 100644 --- a/src/Domain/Event/IFSCEvent.php +++ b/src/Domain/Event/IFSCEvent.php @@ -22,7 +22,7 @@ final class IFSCEvent /** * @param IFSCRound[] $rounds - * @param IFSCStarter[] $starters + * @param IFSCStarter[] $startList * @param IFSCDiscipline[] $disciplines */ public function __construct( @@ -40,7 +40,7 @@ public function __construct( public readonly DateTimeImmutable $endsAt, public readonly array $disciplines, array $rounds, - public readonly array $starters = [], + public readonly array $startList = [], ) { $this->rounds = $rounds; } diff --git a/src/Domain/Event/IFSCEventFactory.php b/src/Domain/Event/IFSCEventFactory.php index 34cc172..1910e4f 100644 --- a/src/Domain/Event/IFSCEventFactory.php +++ b/src/Domain/Event/IFSCEventFactory.php @@ -47,7 +47,7 @@ public function create(IFSCSeasonYear $season, IFSCEventInfo $event, array $roun endsAt: $endDate, disciplines: $event->disciplines, rounds: $rounds, - starters: $this->buildStartList($event->eventId), + startList: $this->buildStartList($event->eventId), ); } diff --git a/src/Domain/Event/IFSCEventsFetcher.php b/src/Domain/Event/IFSCEventsFetcher.php index cbaa93e..f4ad50a 100644 --- a/src/Domain/Event/IFSCEventsFetcher.php +++ b/src/Domain/Event/IFSCEventsFetcher.php @@ -78,7 +78,7 @@ private function generateRounds(IFSCEventInfo $event): array event: $event, roundName: $this->normalizeRoundName($round), startTime: $startTime, - endTime: $startTime->modify('90 minutes'), + endTime: $startTime->modify('+90 minutes'), status: IFSCRoundStatus::ESTIMATED, ); } diff --git a/src/Domain/Round/IFSCRound.php b/src/Domain/Round/IFSCRound.php index ca8150a..09ac802 100644 --- a/src/Domain/Round/IFSCRound.php +++ b/src/Domain/Round/IFSCRound.php @@ -8,21 +8,18 @@ namespace nicoSWD\IfscCalendar\Domain\Round; use DateTimeImmutable; -use nicoSWD\IfscCalendar\Domain\Discipline\IFSCDiscipline; +use nicoSWD\IfscCalendar\Domain\Discipline\IFSCDisciplines; use nicoSWD\IfscCalendar\Domain\Stream\LiveStream; final readonly class IFSCRound { - /** - * @param IFSCRoundCategory[] $categories - * @param IFSCDiscipline[] $disciplines - */ + /** @param IFSCRoundCategory[] $categories */ public function __construct( public string $name, public array $categories, - public array $disciplines, + public IFSCDisciplines $disciplines, public IFSCRoundKind $kind, - public LiveStream $streamUrl, + public LiveStream $liveStream, public DateTimeImmutable $startTime, public DateTimeImmutable $endTime, public IFSCRoundStatus $status, diff --git a/src/Domain/Round/IFSCRoundFactory.php b/src/Domain/Round/IFSCRoundFactory.php index 63d43bd..60a7bbe 100644 --- a/src/Domain/Round/IFSCRoundFactory.php +++ b/src/Domain/Round/IFSCRoundFactory.php @@ -9,6 +9,7 @@ use DateTimeImmutable; use DateTimeInterface; +use nicoSWD\IfscCalendar\Domain\Discipline\IFSCDisciplines; use nicoSWD\IfscCalendar\Domain\Event\Info\IFSCEventInfo; use nicoSWD\IfscCalendar\Domain\Stream\LiveStream; use nicoSWD\IfscCalendar\Domain\Tags\IFSCParsedTags; @@ -44,9 +45,9 @@ public function create( return new IFSCRound( name: $roundName, categories: $tags->getCategories(), - disciplines: $tags->getDisciplines(), + disciplines: $this->getDisciplines($tags), kind: $tags->getRoundKind(), - streamUrl: $liveStream, + liveStream: $liveStream, startTime: $startTime, endTime: $endTime, status: $status, @@ -58,6 +59,11 @@ private function getTags(string $string): IFSCParsedTags return $this->tagsParser->fromString($string); } + private function getDisciplines(IFSCParsedTags $tags): IFSCDisciplines + { + return new IFSCDisciplines($tags->getDisciplines()); + } + private function buildStartTime(LiveStream $liveStream, IFSCEventInfo $event): DateTimeImmutable { $schedulesStartTime = $liveStream->scheduledStartTime->format( diff --git a/src/Domain/Stream/LiveStream.php b/src/Domain/Stream/LiveStream.php index 119f07e..bd5a4ed 100644 --- a/src/Domain/Stream/LiveStream.php +++ b/src/Domain/Stream/LiveStream.php @@ -19,4 +19,9 @@ public function __construct( public array $restrictedRegions = [], ) { } + + public function hasUrl(): bool + { + return $this->url !== null; + } } diff --git a/src/Domain/YouTube/YouTubeLiveStreamFinderFactory.php b/src/Domain/YouTube/YouTubeLiveStreamFinderFactory.php index d8d6727..9efd1f2 100644 --- a/src/Domain/YouTube/YouTubeLiveStreamFinderFactory.php +++ b/src/Domain/YouTube/YouTubeLiveStreamFinderFactory.php @@ -10,7 +10,7 @@ final readonly class YouTubeLiveStreamFinderFactory { public function __construct( - private YouTubeLinkMatcher $linkMatcher, + private YouTubeLinkMatcher $linkMatcher, private YouTubeVideoProvider $linkFetcher, ) { } diff --git a/src/Infrastructure/Calendar/ICalCalendar.php b/src/Infrastructure/Calendar/ICalCalendar.php index 22b8bf4..73cf8b7 100644 --- a/src/Infrastructure/Calendar/ICalCalendar.php +++ b/src/Infrastructure/Calendar/ICalCalendar.php @@ -71,7 +71,7 @@ private function createEvents(array $events): array $calendarEvents = []; foreach ($events as $event) { - $rounds = $this->getNonQualificationRounds($event); + $rounds = $this->getStreamableRounds($event); if (!empty($rounds)) { foreach ($rounds as $round) { @@ -97,26 +97,30 @@ private function createEvent(IFSCEvent $event, IFSCRound $round): Event ->setOccurrence($this->buildTimeSpan($round)); if ($round->status->isConfirmed()) { - $alarm = $this->createAlarmOneHourBefore($event, $round); - } else { - $alarm = $this->createAlarmOneDayBefore($event, $round); + $calendarEvent->addAlarm( + $this->createAlarmOneHourBefore($event, $round->name), + ); } - $calendarEvent->addAlarm($alarm); - return $calendarEvent; } /** @throws Exception */ private function createEventWithoutRounds(IFSCEvent $event): Event { - return (new Event()) + $calendarEvent = (new Event()) ->setSummary(sprintf('%s (%s)', $event->normalizedName(), $event->country)) ->setDescription($this->buildDescription($event)) ->setUrl(new Uri($event->siteUrl)) ->setStatus(EventStatus::TENTATIVE()) ->setLocation(new Location("{$event->location} ({$event->country})")) ->setOccurrence($this->buildGenericTimeSpan($event)); + + $calendarEvent->addAlarm( + $this->createAlarmOneDayBefore($event, $event->eventName), + ); + + return $calendarEvent; } private function buildTimeSpan(IFSCRound $round): TimeSpan @@ -140,7 +144,10 @@ private function buildDescription(IFSCEvent $event, ?IFSCRound $round = null): s { $description = "🏆 {$event->normalizedName()} ({$event->country})\n\n"; - if (!$round?->status->isConfirmed()) { + if ($round?->status->isProvisional()) { + $description .= "⚠️ Schedule is provisional and might change. "; + $description .= "This calendar will update automatically once it's confirmed!\n\n"; + } elseif ($round === null) { $description .= "⚠️ Precise schedule has not been announced yet. "; $description .= "This calendar will update automatically once it's published!\n\n"; } @@ -149,11 +156,11 @@ private function buildDescription(IFSCEvent $event, ?IFSCRound $round = null): s $description .= "🍿 Stream URL:\n{$event->siteUrl}\n\n"; $description .= "💬 Join Discord:\n" . self::DISCORD_URL . "\n"; - if ($event->starters) { + if ($event->startList) { $description .= "\n📋 Start List:\n"; - foreach ($event->starters as $starter) { - $description .= " - {$starter->firstName} {$starter->lastName} ({$starter->country})\n"; + foreach ($event->startList as $athlete) { + $description .= " - {$athlete->firstName} {$athlete->lastName} ({$athlete->country})\n"; } $description .= " - ...\n"; @@ -176,22 +183,29 @@ private function getEventStatus(IFSCRound $round): EventStatus } /** @return IFSCRound[] */ - private function getNonQualificationRounds(IFSCEvent $event): array + private function getStreamableRounds(IFSCEvent $event): array + { + return array_filter($event->rounds, fn (IFSCRound $round): bool => $this->roundIsStreamable($round)); + } + + private function roundIsStreamable(IFSCRound $round): bool { - return array_filter($event->rounds, static fn (IFSCRound $round): bool => !$round->kind->isQualification()); + return + !$round->kind->isQualification() || + ($round->disciplines->isSpeed() && $round->liveStream->hasUrl()); } - private function createAlarmOneHourBefore(IFSCEvent $event, IFSCRound $round): Alarm + private function createAlarmOneHourBefore(IFSCEvent $event, string $name): Alarm { - return $this->createAlarm($event, $round, timeBefore: '1 hour'); + return $this->createAlarm($event, $name, timeBefore: '1 hour'); } - private function createAlarmOneDayBefore(IFSCEvent $event, IFSCRound $round): Alarm + private function createAlarmOneDayBefore(IFSCEvent $event, string $name): Alarm { - return $this->createAlarm($event, $round, timeBefore: '1 day'); + return $this->createAlarm($event, $name, timeBefore: '1 day'); } - private function createAlarm(IFSCEvent $event, IFSCRound $round, string $timeBefore): Alarm + private function createAlarm(IFSCEvent $event, string $name, string $timeBefore): Alarm { $trigger = new RelativeTrigger( DateInterval::createFromDateString(datetime: "-{$timeBefore}"), @@ -199,7 +213,7 @@ private function createAlarm(IFSCEvent $event, IFSCRound $round, string $timeBef return new Alarm( new DisplayAction( - description: "Reminder: IFSC: {$round->name} - {$event->location} ({$event->country}) starts in {$timeBefore}!" + description: "Reminder: IFSC: {$name} - {$event->location} ({$event->country}) starts in {$timeBefore}!" ), $trigger->withRelationToEnd(), ); diff --git a/src/Infrastructure/Calendar/JsonCalendar.php b/src/Infrastructure/Calendar/JsonCalendar.php index feac4fe..b5320f2 100644 --- a/src/Infrastructure/Calendar/JsonCalendar.php +++ b/src/Infrastructure/Calendar/JsonCalendar.php @@ -54,7 +54,7 @@ 'ends_at' => $this->formatDate($event->endsAt), 'timezone' => $event->timeZone->getName(), 'rounds' => $this->formatRound($event->rounds), - 'start_list' => $this->formatStarters($event->starters), + 'start_list' => $this->formatStarters($event->startList), ]; } @@ -75,8 +75,8 @@ private function formatRound(array $rounds): array 'starts_at' => $this->formatDate($round->startTime), 'ends_at' => $this->formatDate($round->endTime), 'schedule_status' => $round->status->value, - 'stream_url' => $round->streamUrl->url, - 'stream_blocked_regions' => $round->streamUrl->restrictedRegions, + 'stream_url' => $round->liveStream->url, + 'stream_blocked_regions' => $round->liveStream->restrictedRegions, ]; return array_map($format, $rounds); @@ -106,7 +106,7 @@ private function buildUrl(IFSCEvent $event): string /** @return string[] */ private function buildDisciplines(IFSCRound $round): array { - return array_map(static fn (IFSCDiscipline $discipline): string => $discipline->value, $round->disciplines); + return array_map(static fn (IFSCDiscipline $discipline): string => $discipline->value, $round->disciplines->all()); } /** @return string[] */ From 0b28af1842a5f2ff0b52bcd133f089a857ed2d5e Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Fri, 12 Apr 2024 22:17:53 +0200 Subject: [PATCH 24/32] Add proper average round duration --- bin/avg-round-duration | 43 ++++++++++ config/services/domain.yml | 4 + src/Domain/Round/IFSCAverageRoundDuration.php | 81 +++++++++++++++++++ src/Domain/Round/IFSCRoundFactory.php | 30 +++---- src/Domain/Schedule/IFSCSchedule.php | 2 +- src/Domain/Schedule/IFSCScheduleFactory.php | 2 +- .../Schedule/InfoSheetScheduleProvider.php | 6 +- 7 files changed, 149 insertions(+), 19 deletions(-) create mode 100644 bin/avg-round-duration create mode 100644 src/Domain/Round/IFSCAverageRoundDuration.php diff --git a/bin/avg-round-duration b/bin/avg-round-duration new file mode 100644 index 0000000..16f3db2 --- /dev/null +++ b/bin/avg-round-duration @@ -0,0 +1,43 @@ +#!/usr/bin/env php +name; + } + + return strtolower(implode('_', $key)); +} + +$videoDurations = []; + +foreach ($videos as $video) { + $tags = $tagsParser->fromString($video->title); + $videoDurations[cache_key($tags->allTags())][] = $video->duration; +} + +$results = []; + +foreach ($videoDurations as $key => $minutes) { + $averageDuration = (int) floor(array_sum($minutes) / count($minutes)); + $averageDuration -= ($averageDuration % 30); + + if ($averageDuration > 30) { + $results[$key] = $averageDuration; + } +} + +var_export($results); diff --git a/config/services/domain.yml b/config/services/domain.yml index 67ea8f7..49cfa71 100644 --- a/config/services/domain.yml +++ b/config/services/domain.yml @@ -100,11 +100,15 @@ services: - '@nicoSWD\IfscCalendar\Domain\Round\IFSCRoundFactory' - '@nicoSWD\IfscCalendar\Infrastructure\Round\InfoSheetRoundProvider' + nicoSWD\IfscCalendar\Domain\Round\IFSCAverageRoundDuration: + class: nicoSWD\IfscCalendar\Domain\Round\IFSCAverageRoundDuration + nicoSWD\IfscCalendar\Domain\Round\IFSCRoundFactory: class: nicoSWD\IfscCalendar\Domain\Round\IFSCRoundFactory arguments: - '@nicoSWD\IfscCalendar\Domain\Tags\IFSCTagsParser' - '@nicoSWD\IfscCalendar\Domain\YouTube\YouTubeLiveStreamFinder' + - '@nicoSWD\IfscCalendar\Domain\Round\IFSCAverageRoundDuration' nicoSWD\IfscCalendar\Domain\Round\IFSCRoundNameNormalizer: class: nicoSWD\IfscCalendar\Domain\Round\IFSCRoundNameNormalizer diff --git a/src/Domain/Round/IFSCAverageRoundDuration.php b/src/Domain/Round/IFSCAverageRoundDuration.php new file mode 100644 index 0000000..8a3629d --- /dev/null +++ b/src/Domain/Round/IFSCAverageRoundDuration.php @@ -0,0 +1,81 @@ + + */ +namespace nicoSWD\IfscCalendar\Domain\Round; + +use nicoSWD\IfscCalendar\Domain\Event\IFSCEventTagsRegex as Tag; + +final readonly class IFSCAverageRoundDuration +{ + private const int DEFAULT_ROUND_DURATION = 90; + + private const array AVERAGE_DURATIONS = [ + 'men_boulder_final' => 120, + 'men_boulder_semi_final' => 120, + 'women_boulder_final' => 120, + 'women_boulder_semi_final' => 120, + 'lead_final' => 120, + 'speed_final' => 90, + 'lead_semi_final' => 150, + 'speed_qualification' => 90, + 'women_lead_boulder_semi_final' => 90, + 'men_lead_boulder_final' => 180, + 'women_lead_boulder_final' => 180, + 'men_lead_boulder_semi_final' => 180, + 'lead_boulder_final' => 240, + 'lead_boulder_semi_final' => 120, + 'women_men_boulder_final' => 180, + 'women_men_boulder_semi_final' => 150, + 'women_men_speed_final' => 60, + 'women_men_lead_final' => 120, + 'women_men_lead_semi_final' => 150, + 'paraclimbing_final' => 210, + 'men_speed_qualification' => 60, + 'women_speed_final' => 60, + 'boulder_final' => 210, + 'men_lead_final' => 60, + 'men_lead_semi_final' => 120, + 'women_lead_final' => 120, + 'women_lead_semi_final' => 210, + 'boulder_semi_final' => 120, + 'women_men_lead_boulder_semi_final_final' => 360, + 'combined_final' => 420, + 'lead_combined_qualification' => 90, + 'boulder_combined_qualification' => 120, + 'speed_combined_qualification' => 60, + 'men_combined_final' => 240, + 'women_combined_final' => 240, + 'women_boulder_combined_qualification' => 120, + 'men_lead_combined_qualification' => 90, + 'men_boulder_combined_qualification' => 120, + 'women_lead_combined_qualification' => 120, + 'women_final' => 120, + 'women_men_paraclimbing_final' => 150, + 'paraclimbing' => 120, + 'men_boulder_paraclimbing_final' => 150, + ]; + + /** @param Tag[] $tags */ + public function fromTags(array $tags): int + { + return self::AVERAGE_DURATIONS[$this->lookupKey($tags)] + ?? self::DEFAULT_ROUND_DURATION; + } + + /** @param Tag[] $tags */ + private function lookupKey(array $tags): string + { + $lookupKey = implode('_', + array_map( + static fn (Tag $tag): string => $tag->name, + $tags, + ) + ); + + return strtolower($lookupKey); + } +} diff --git a/src/Domain/Round/IFSCRoundFactory.php b/src/Domain/Round/IFSCRoundFactory.php index 60a7bbe..6c41a96 100644 --- a/src/Domain/Round/IFSCRoundFactory.php +++ b/src/Domain/Round/IFSCRoundFactory.php @@ -18,11 +18,10 @@ final readonly class IFSCRoundFactory { - private const int DEFAULT_ROUND_DURATION = 90; - public function __construct( private IFSCTagsParser $tagsParser, private YouTubeLiveStreamFinder $liveStreamFinder, + private IFSCAverageRoundDuration $averageRoundDuration, ) { } @@ -30,18 +29,29 @@ public function create( IFSCEventInfo $event, string $roundName, DateTimeImmutable $startTime, - DateTimeImmutable $endTime, + ?DateTimeImmutable $endTime, IFSCRoundStatus $status, ): IFSCRound { $tags = $this->getTags($roundName); $liveStream = $this->findLiveStream($event, $roundName); if ($liveStream->scheduledStartTime) { + if (!$endTime && $liveStream->duration > 0) { + $endTime = $liveStream->scheduledStartTime->modify( + sprintf('+%d minutes', $liveStream->duration), + ); + } + $startTime = $this->buildStartTime($liveStream, $event); - $endTime = $this->buildEndTime($startTime, $liveStream); $status = IFSCRoundStatus::CONFIRMED; } + if (!$endTime) { + $endTime = $startTime->modify( + sprintf('+%d minutes', $this->averageRoundDuration($tags)), + ); + } + return new IFSCRound( name: $roundName, categories: $tags->getCategories(), @@ -80,16 +90,8 @@ private function findLiveStream(IFSCEventInfo $event, string $roundName): LiveSt return $this->liveStreamFinder->findLiveStream($event, $roundName); } - private function buildEndTime(DateTimeImmutable $startTime, LiveStream $liveStream): DateTimeImmutable + private function averageRoundDuration(IFSCParsedTags $tags): int { - if ($liveStream->duration > 0) { - $duration = $liveStream->duration; - } else { - $duration = self::DEFAULT_ROUND_DURATION; - } - - return $startTime->modify( - sprintf('+%d minutes', $duration) - ); + return $this->averageRoundDuration->fromTags($tags->allTags()); } } diff --git a/src/Domain/Schedule/IFSCSchedule.php b/src/Domain/Schedule/IFSCSchedule.php index 5a0dc73..55fc3e5 100644 --- a/src/Domain/Schedule/IFSCSchedule.php +++ b/src/Domain/Schedule/IFSCSchedule.php @@ -14,7 +14,7 @@ public function __construct( public string $name, public DateTimeImmutable $startsAt, - public DateTimeImmutable $endsAt, + public ?DateTimeImmutable $endsAt, public bool $isPreRound, ) { } diff --git a/src/Domain/Schedule/IFSCScheduleFactory.php b/src/Domain/Schedule/IFSCScheduleFactory.php index 72e0c8b..35f869c 100644 --- a/src/Domain/Schedule/IFSCScheduleFactory.php +++ b/src/Domain/Schedule/IFSCScheduleFactory.php @@ -23,7 +23,7 @@ public function __construct( public function create( string $name, DateTimeImmutable $startsAt, - DateTimeImmutable $endsAt, + ?DateTimeImmutable $endsAt, ): IFSCSchedule { $tags = $this->tagsParser->fromString($name); diff --git a/src/Infrastructure/Schedule/InfoSheetScheduleProvider.php b/src/Infrastructure/Schedule/InfoSheetScheduleProvider.php index 3c5ed75..76870d6 100644 --- a/src/Infrastructure/Schedule/InfoSheetScheduleProvider.php +++ b/src/Infrastructure/Schedule/InfoSheetScheduleProvider.php @@ -80,7 +80,7 @@ private function parseDaySchedule(string $schedule, DateTimeZone $timeZone): Ite $startsAt = $this->createStartDate($dayName, $match['start_time'][$key], $timeZone); } - $endsAt = $this->createEndDate($dayName, $match['end_time'][$key] ?? null, $startsAt, $timeZone); + $endsAt = $this->createEndDate($dayName, $match['end_time'][$key] ?? null, $timeZone); $lastStart = $startsAt; $schedule = $this->scheduleFactory->create( @@ -113,13 +113,13 @@ private function createStartDate(string $day, string $time, DateTimeZone $timeZo ); } - private function createEndDate(string $dayName, ?string $time, DateTimeImmutable $startsAt, DateTimeZone $timeZone): DateTimeImmutable + private function createEndDate(string $dayName, ?string $time, DateTimeZone $timeZone): ?DateTimeImmutable { if ($time !== null && trim($time) !== '') { return $this->createStartDate($dayName, $time, $timeZone); } - return $startsAt->modify('+2 hours'); + return null; } /** @return string[] */ From 2785df7a459bbd67f31dd23bbbda6694eb37313d Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Fri, 12 Apr 2024 22:47:06 +0200 Subject: [PATCH 25/32] Fix tests --- .../Schedule/PDFScheduleProviderTest.php | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/unit/Infrastructure/Schedule/PDFScheduleProviderTest.php b/tests/unit/Infrastructure/Schedule/PDFScheduleProviderTest.php index 630a67d..f51073d 100644 --- a/tests/unit/Infrastructure/Schedule/PDFScheduleProviderTest.php +++ b/tests/unit/Infrastructure/Schedule/PDFScheduleProviderTest.php @@ -30,27 +30,27 @@ final class PDFScheduleProviderTest extends TestCase $this->assertSame("Women's Boulder Qualification", $schedule[0]->name); $this->assertSameDate("2024-04-08T09:00:00+08:00", $schedule[0]->startsAt); - $this->assertSameDate("2024-04-08T11:00:00+08:00", $schedule[0]->endsAt); + $this->assertNull($schedule[0]->endsAt); $this->assertSame("Men's Boulder Qualification", $schedule[1]->name); $this->assertSameDate("2024-04-08T16:00:00+08:00", $schedule[1]->startsAt); - $this->assertSameDate("2024-04-08T18:00:00+08:00", $schedule[1]->endsAt); + $this->assertNull($schedule[1]->endsAt); $this->assertSame("Women's Boulder Semi-Final", $schedule[2]->name); $this->assertSameDate("2024-04-09T12:00:00+08:00", $schedule[2]->startsAt); - $this->assertSameDate("2024-04-09T14:00:00+08:00", $schedule[2]->endsAt); + $this->assertNull($schedule[2]->endsAt); $this->assertSame("Women's Boulder Final", $schedule[3]->name); $this->assertSameDate("2024-04-09T19:00:00+08:00", $schedule[3]->startsAt); - $this->assertSameDate("2024-04-09T21:00:00+08:00", $schedule[3]->endsAt); + $this->assertNull($schedule[3]->endsAt); $this->assertSame("Men's Boulder Semi-Final", $schedule[4]->name); $this->assertSameDate("2024-04-10T12:00:00+08:00", $schedule[4]->startsAt); - $this->assertSameDate("2024-04-10T14:00:00+08:00", $schedule[4]->endsAt); + $this->assertNull($schedule[4]->endsAt); $this->assertSame("Men's Boulder Final", $schedule[5]->name); $this->assertSameDate("2024-04-10T19:00:00+08:00", $schedule[5]->startsAt); - $this->assertSameDate("2024-04-10T21:00:00+08:00", $schedule[5]->endsAt); + $this->assertNull($schedule[5]->endsAt); } #[Test] public function wujiang_schedule_is_found(): void @@ -65,23 +65,23 @@ final class PDFScheduleProviderTest extends TestCase $this->assertSame("Men's & Women's Speed Qualification", $schedule[1]->name); $this->assertSameDate("2024-04-12T19:00:00+08:00", $schedule[1]->startsAt); - $this->assertSameDate("2024-04-12T21:00:00+08:00", $schedule[1]->endsAt); + $this->assertNull($schedule[1]->endsAt); $this->assertSame("Men's & Women's Lead Semi-Final", $schedule[2]->name); $this->assertSameDate("2024-04-13T15:00:00+08:00", $schedule[2]->startsAt); - $this->assertSameDate("2024-04-13T17:00:00+08:00", $schedule[2]->endsAt); + $this->assertNull($schedule[2]->endsAt); $this->assertSame("Men's & Women's Speed Final", $schedule[3]->name); $this->assertSameDate("2024-04-13T19:30:00+08:00", $schedule[3]->startsAt); - $this->assertSameDate("2024-04-13T21:30:00+08:00", $schedule[3]->endsAt); + $this->assertNull($schedule[3]->endsAt); $this->assertSame("Women's Lead Final", $schedule[4]->name); $this->assertSameDate("2024-04-14T19:00:00+08:00", $schedule[4]->startsAt); - $this->assertSameDate("2024-04-14T21:00:00+08:00", $schedule[4]->endsAt); + $this->assertNull($schedule[4]->endsAt); $this->assertSame("Men's Lead Final", $schedule[5]->name); $this->assertSameDate("2024-04-14T20:00:00+08:00", $schedule[5]->startsAt); - $this->assertSameDate("2024-04-14T22:00:00+08:00", $schedule[5]->endsAt); + $this->assertNull($schedule[5]->endsAt); } #[Test] public function salt_lake_city_schedule_is_found(): void @@ -108,11 +108,11 @@ final class PDFScheduleProviderTest extends TestCase $this->assertSame("Men's Boulder Final", $schedule[4]->name); $this->assertSameDate("2024-05-04T18:00:00-07:00", $schedule[4]->startsAt); - $this->assertSameDate("2024-05-04T20:00:00-07:00", $schedule[4]->endsAt); + $this->assertNull($schedule[4]->endsAt); $this->assertSame("Women's Speed Final", $schedule[5]->name); $this->assertSameDate("2024-05-04T20:00:00-07:00", $schedule[5]->startsAt); - $this->assertSameDate("2024-05-04T22:00:00-07:00", $schedule[5]->endsAt); + $this->assertNull($schedule[5]->endsAt); $this->assertSame("Women's Boulder Semi-Final", $schedule[6]->name); $this->assertSameDate("2024-05-05T10:00:00-07:00", $schedule[6]->startsAt); @@ -124,11 +124,11 @@ final class PDFScheduleProviderTest extends TestCase $this->assertSame("Women's Boulder Final", $schedule[8]->name); $this->assertSameDate("2024-05-05T18:00:00-07:00", $schedule[8]->startsAt); - $this->assertSameDate("2024-05-05T20:00:00-07:00", $schedule[8]->endsAt); + $this->assertNull($schedule[8]->endsAt); $this->assertSame("Men's Speed Final", $schedule[9]->name); $this->assertSameDate("2024-05-05T20:00:00-07:00", $schedule[9]->startsAt); - $this->assertSameDate("2024-05-05T22:00:00-07:00", $schedule[9]->endsAt); + $this->assertNull($schedule[9]->endsAt); } #[Test] public function innsbruck_schedule_is_found(): void @@ -194,11 +194,11 @@ final class PDFScheduleProviderTest extends TestCase $this->assertSame("Men's Lead Final", $schedule[2]->name); $this->assertSameDate("2024-09-07T20:00:00+02:00", $schedule[2]->startsAt); - $this->assertSameDate("2024-09-07T22:00:00+02:00", $schedule[2]->endsAt); + $this->assertNull($schedule[2]->endsAt); $this->assertSame("Women's Lead Final", $schedule[3]->name); $this->assertSameDate("2024-09-07T21:00:00+02:00", $schedule[3]->startsAt); - $this->assertSameDate("2024-09-07T23:00:00+02:00", $schedule[3]->endsAt); + $this->assertNull($schedule[3]->endsAt); } #[Test] public function chamonix_schedule_is_found(): void @@ -209,7 +209,7 @@ final class PDFScheduleProviderTest extends TestCase $this->assertSame("Men's & Women's Speed Qualification", $schedule[0]->name); $this->assertSameDate("2024-07-12T18:45:00+02:00", $schedule[0]->startsAt); - $this->assertSameDate("2024-07-12T20:45:00+02:00", $schedule[0]->endsAt); + $this->assertNull($schedule[0]->endsAt); $this->assertSame("Men's & Women's Lead Qualification", $schedule[1]->name); $this->assertSameDate("2024-07-13T09:00:00+02:00", $schedule[1]->startsAt); @@ -217,7 +217,7 @@ final class PDFScheduleProviderTest extends TestCase $this->assertSame("Men's & Women's Speed Final", $schedule[2]->name); $this->assertSameDate("2024-07-13T21:00:00+02:00", $schedule[2]->startsAt); - $this->assertSameDate("2024-07-13T23:00:00+02:00", $schedule[2]->endsAt); + $this->assertNull($schedule[2]->endsAt); $this->assertSame("Men's & Women's Lead Semi-Final", $schedule[3]->name); $this->assertSameDate("2024-07-14T10:00:00+02:00", $schedule[3]->startsAt); @@ -225,11 +225,11 @@ final class PDFScheduleProviderTest extends TestCase $this->assertSame("Women's Lead Final", $schedule[4]->name); $this->assertSameDate("2024-07-14T20:30:00+02:00", $schedule[4]->startsAt); - $this->assertSameDate("2024-07-14T22:30:00+02:00", $schedule[4]->endsAt); + $this->assertNull($schedule[4]->endsAt); $this->assertSame("Men's Lead Final", $schedule[5]->name); $this->assertSameDate("2024-07-14T21:25:00+02:00", $schedule[5]->startsAt); - $this->assertSameDate("2024-07-14T23:25:00+02:00", $schedule[5]->endsAt); + $this->assertNull($schedule[5]->endsAt); } #[Test] public function prague_schedule_is_found(): void @@ -240,27 +240,27 @@ final class PDFScheduleProviderTest extends TestCase $this->assertSame("Men's Boulder Qualification", $schedule[0]->name); $this->assertSameDate("2024-09-20T09:00:00+02:00", $schedule[0]->startsAt); - $this->assertSameDate("2024-09-20T11:00:00+02:00", $schedule[0]->endsAt); + $this->assertNull($schedule[0]->endsAt); $this->assertSame("Women's Boulder Qualification", $schedule[1]->name); $this->assertSameDate("2024-09-20T16:00:00+02:00", $schedule[1]->startsAt); - $this->assertSameDate("2024-09-20T18:00:00+02:00", $schedule[1]->endsAt); + $this->assertNull($schedule[1]->endsAt); $this->assertSame("Men's Boulder Semi-Final", $schedule[2]->name); $this->assertSameDate("2024-09-21T12:00:00+02:00", $schedule[2]->startsAt); - $this->assertSameDate("2024-09-21T14:00:00+02:00", $schedule[2]->endsAt); + $this->assertNull($schedule[2]->endsAt); $this->assertSame("Men's Boulder Final", $schedule[3]->name); $this->assertSameDate("2024-09-21T20:00:00+02:00", $schedule[3]->startsAt); - $this->assertSameDate("2024-09-21T22:00:00+02:00", $schedule[3]->endsAt); + $this->assertNull($schedule[3]->endsAt); $this->assertSame("Women's Boulder Semi-Final", $schedule[4]->name); $this->assertSameDate("2024-09-22T12:00:00+02:00", $schedule[4]->startsAt); - $this->assertSameDate("2024-09-22T14:00:00+02:00", $schedule[4]->endsAt); + $this->assertNull($schedule[4]->endsAt); $this->assertSame("Women's Boulder Final", $schedule[5]->name); $this->assertSameDate("2024-09-22T19:00:00+02:00", $schedule[5]->startsAt); - $this->assertSameDate("2024-09-22T21:00:00+02:00", $schedule[5]->endsAt); + $this->assertNull($schedule[5]->endsAt); } private function assertSameDate(string $expected, DateTimeImmutable $actual): void From b7cc1b87f1bd51774c7d8166e2bbb1292d9da7d7 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Fri, 12 Apr 2024 22:48:46 +0200 Subject: [PATCH 26/32] Removed unused constant --- src/Domain/Round/IFSCRoundFactory.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Domain/Round/IFSCRoundFactory.php b/src/Domain/Round/IFSCRoundFactory.php index 2abb1ae..6c41a96 100644 --- a/src/Domain/Round/IFSCRoundFactory.php +++ b/src/Domain/Round/IFSCRoundFactory.php @@ -18,8 +18,6 @@ final readonly class IFSCRoundFactory { - private const int DEFAULT_ROUND_DURATION = 90; - public function __construct( private IFSCTagsParser $tagsParser, private YouTubeLiveStreamFinder $liveStreamFinder, From bdd2cae5ab1785138fbb3ffbe87c9cda4103380a Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Fri, 12 Apr 2024 23:08:37 +0200 Subject: [PATCH 27/32] Always use YT duration --- src/Domain/Round/IFSCRoundFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Round/IFSCRoundFactory.php b/src/Domain/Round/IFSCRoundFactory.php index 6c41a96..f011d6b 100644 --- a/src/Domain/Round/IFSCRoundFactory.php +++ b/src/Domain/Round/IFSCRoundFactory.php @@ -36,7 +36,7 @@ public function create( $liveStream = $this->findLiveStream($event, $roundName); if ($liveStream->scheduledStartTime) { - if (!$endTime && $liveStream->duration > 0) { + if ($liveStream->duration > 0) { $endTime = $liveStream->scheduledStartTime->modify( sprintf('+%d minutes', $liveStream->duration), ); From 648546d6d368f00440f6a62943e4295d9ee823a3 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Sat, 13 Apr 2024 01:37:32 +0200 Subject: [PATCH 28/32] Fix start time issue and add tests --- src/Domain/Round/IFSCRoundFactory.php | 9 +- .../YouTube/YouTubeLiveStreamFinder.php | 2 +- .../YouTubeLiveStreamFinderInterface.php | 16 +++ .../Domain/Round/IFSCRoundFactoryTest.php | 136 ++++++++++++++++++ 4 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 src/Domain/YouTube/YouTubeLiveStreamFinderInterface.php create mode 100644 tests/unit/Domain/Round/IFSCRoundFactoryTest.php diff --git a/src/Domain/Round/IFSCRoundFactory.php b/src/Domain/Round/IFSCRoundFactory.php index f011d6b..5ac7837 100644 --- a/src/Domain/Round/IFSCRoundFactory.php +++ b/src/Domain/Round/IFSCRoundFactory.php @@ -14,13 +14,13 @@ use nicoSWD\IfscCalendar\Domain\Stream\LiveStream; use nicoSWD\IfscCalendar\Domain\Tags\IFSCParsedTags; use nicoSWD\IfscCalendar\Domain\Tags\IFSCTagsParser; -use nicoSWD\IfscCalendar\Domain\YouTube\YouTubeLiveStreamFinder; +use nicoSWD\IfscCalendar\Domain\YouTube\YouTubeLiveStreamFinderInterface; final readonly class IFSCRoundFactory { public function __construct( private IFSCTagsParser $tagsParser, - private YouTubeLiveStreamFinder $liveStreamFinder, + private YouTubeLiveStreamFinderInterface $liveStreamFinder, private IFSCAverageRoundDuration $averageRoundDuration, ) { } @@ -36,13 +36,14 @@ public function create( $liveStream = $this->findLiveStream($event, $roundName); if ($liveStream->scheduledStartTime) { + $startTime = $this->buildStartTime($liveStream, $event); + if ($liveStream->duration > 0) { - $endTime = $liveStream->scheduledStartTime->modify( + $endTime = $startTime->modify( sprintf('+%d minutes', $liveStream->duration), ); } - $startTime = $this->buildStartTime($liveStream, $event); $status = IFSCRoundStatus::CONFIRMED; } diff --git a/src/Domain/YouTube/YouTubeLiveStreamFinder.php b/src/Domain/YouTube/YouTubeLiveStreamFinder.php index 7f44f77..f3354dc 100644 --- a/src/Domain/YouTube/YouTubeLiveStreamFinder.php +++ b/src/Domain/YouTube/YouTubeLiveStreamFinder.php @@ -10,7 +10,7 @@ use nicoSWD\IfscCalendar\Domain\Event\Info\IFSCEventInfo; use nicoSWD\IfscCalendar\Domain\Stream\LiveStream; -final readonly class YouTubeLiveStreamFinder +final readonly class YouTubeLiveStreamFinder implements YouTubeLiveStreamFinderInterface { public function __construct( private YouTubeLinkMatcher $linkMatcher, diff --git a/src/Domain/YouTube/YouTubeLiveStreamFinderInterface.php b/src/Domain/YouTube/YouTubeLiveStreamFinderInterface.php new file mode 100644 index 0000000..2f7d851 --- /dev/null +++ b/src/Domain/YouTube/YouTubeLiveStreamFinderInterface.php @@ -0,0 +1,16 @@ + + */ +namespace nicoSWD\IfscCalendar\Domain\YouTube; + +use nicoSWD\IfscCalendar\Domain\Event\Info\IFSCEventInfo; +use nicoSWD\IfscCalendar\Domain\Stream\LiveStream; + +interface YouTubeLiveStreamFinderInterface +{ + public function findLiveStream(IFSCEventInfo $event, string $roundName): LiveStream; +} diff --git a/tests/unit/Domain/Round/IFSCRoundFactoryTest.php b/tests/unit/Domain/Round/IFSCRoundFactoryTest.php new file mode 100644 index 0000000..bdeb712 --- /dev/null +++ b/tests/unit/Domain/Round/IFSCRoundFactoryTest.php @@ -0,0 +1,136 @@ + + */ +namespace nicoSWD\IfscCalendar\tests\Domain\Round; + +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; +use nicoSWD\IfscCalendar\Domain\Event\Info\IFSCEventInfo; +use nicoSWD\IfscCalendar\Domain\Round\IFSCAverageRoundDuration; +use nicoSWD\IfscCalendar\Domain\Round\IFSCRoundFactory; +use nicoSWD\IfscCalendar\Domain\Round\IFSCRoundStatus; +use nicoSWD\IfscCalendar\Domain\Stream\LiveStream; +use nicoSWD\IfscCalendar\Domain\Tags\IFSCTagsParser; +use nicoSWD\IfscCalendar\Domain\YouTube\YouTubeLiveStreamFinderInterface; +use Override; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +final class IFSCRoundFactoryTest extends TestCase +{ + #[Test] public function finished_live_stream_start_and_end_time_is_used(): void + { + $roundFactory = $this->roundFactoryWithLiveStreamWith( + scheduledStartTime: '2024-04-12T10:55:00Z', + duration: 60, + ); + + $round = $roundFactory->create( + event: $this->createEvent(), + roundName: "Men's & Women's Lead Qualification", + startTime: $this->createDateTime('2024-04-12 18:30'), + endTime: $this->createDateTime('2024-04-12 21:23'), + status: IFSCRoundStatus::PROVISIONAL, + ); + + $this->assertSame(IFSCRoundStatus::CONFIRMED, $round->status); + $this->assertSame('2024-04-12T19:00:00+08:00', $this->formatDate($round->startTime)); + $this->assertSame('2024-04-12T20:00:00+08:00', $this->formatDate($round->endTime)); + } + + #[Test] public function average_round_duration_is_used_when_not_streamed_yet(): void + { + $roundFactory = $this->roundFactoryWithLiveStreamWith( + scheduledStartTime: '2024-04-12T10:55:00Z', + duration: 0, + ); + + $round = $roundFactory->create( + event: $this->createEvent(), + roundName: "Men's & Women's Lead Qualification", + startTime: $this->createDateTime('2024-04-12 18:30'), + endTime: null, + status: IFSCRoundStatus::PROVISIONAL, + ); + + $this->assertSame(IFSCRoundStatus::CONFIRMED, $round->status); + $this->assertSame('2024-04-12T19:00:00+08:00', $this->formatDate($round->startTime)); + $this->assertSame('2024-04-12T20:30:00+08:00', $this->formatDate($round->endTime)); + } + + #[Test] public function round_schedule_is_not_confirmed_unless_a_live_stream_was_found(): void + { + $roundFactory = $this->roundFactoryWithLiveStreamWith( + scheduledStartTime: null, + duration: 0, + ); + + $round = $roundFactory->create( + event: $this->createEvent(), + roundName: "Men's & Women's Lead Qualification", + startTime: $this->createDateTime('2024-04-12 18:30'), + endTime: null, + status: IFSCRoundStatus::PROVISIONAL, + ); + + $this->assertSame(IFSCRoundStatus::PROVISIONAL, $round->status); + $this->assertSame('2024-04-13T02:30:00+08:00', $this->formatDate($round->startTime)); + $this->assertSame('2024-04-13T04:00:00+08:00', $this->formatDate($round->endTime)); + } + + private function roundFactoryWithLiveStreamWith(?string $scheduledStartTime, int $duration): IFSCRoundFactory + { + return new IFSCRoundFactory( + new IFSCTagsParser(), + new readonly class ($scheduledStartTime, $duration) implements YouTubeLiveStreamFinderInterface { + public function __construct( + private ?string $scheduledStartTime, + private int $duration, + ) { + } + + #[Override] public function findLiveStream(IFSCEventInfo $event, string $roundName): LiveStream + { + return new LiveStream( + scheduledStartTime: $this->scheduledStartTime ? new DateTimeImmutable($this->scheduledStartTime) : null, + duration: $this->duration, + ); + } + }, + new IFSCAverageRoundDuration(), + ); + } + + private function createEvent(): IFSCEventInfo + { + return new IFSCEventInfo( + eventId: 1292, + eventName: 'IFSC World Cup Salt Lake City 2024', + leagueId: 37, + leagueName: 'World Cups and World Championships', + leagueSeasonId: 12, + localStartDate: '2023-04-10T11:55:00Z', + localEndDate: '2023-04-10T13:55:00Z', + timeZone: new DateTimeZone('Asia/Shanghai'), + location: 'Barcelona', + country: 'ES', + disciplines: [], + categories: [], + ); + } + + private function formatDate(DateTimeImmutable $dateTime): string + { + return $dateTime->format(DateTimeInterface::RFC3339); + } + + private function createDateTime(string $dateTime): DateTimeImmutable + { + return (new DateTimeImmutable($dateTime))->setTimezone(new DateTimeZone('Asia/Shanghai')); + } +} From 51f11fa9e4f5496353e529767b0f5c76bcd05d32 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Sat, 13 Apr 2024 12:05:49 +0200 Subject: [PATCH 29/32] Add missing category tags and improve matching --- bin/avg-round-duration | 13 ++- src/Domain/Round/IFSCAverageRoundDuration.php | 93 +++++++++---------- src/Domain/Tags/IFSCParsedTags.php | 2 +- .../Domain/Round/IFSCRoundFactoryTest.php | 8 +- 4 files changed, 61 insertions(+), 55 deletions(-) mode change 100644 => 100755 bin/avg-round-duration diff --git a/bin/avg-round-duration b/bin/avg-round-duration old mode 100644 new mode 100755 index 16f3db2..6cd9da9 --- a/bin/avg-round-duration +++ b/bin/avg-round-duration @@ -1,6 +1,7 @@ #!/usr/bin/env php name; } + sort($key); + return strtolower(implode('_', $key)); } @@ -26,7 +29,14 @@ $videoDurations = []; foreach ($videos as $video) { $tags = $tagsParser->fromString($video->title); - $videoDurations[cache_key($tags->allTags())][] = $video->duration; + + if (!$tags->hasTag(Tag::WOMEN) && !$tags->hasTag(Tag::MEN)) { + $videoTags = [Tag::WOMEN, Tag::MEN, ...$tags->allTags()]; + } else { + $videoTags = $tags->allTags(); + } + + $videoDurations[cache_key($videoTags)][] = $video->duration; } $results = []; @@ -40,4 +50,5 @@ foreach ($videoDurations as $key => $minutes) { } } +ksort($results); var_export($results); diff --git a/src/Domain/Round/IFSCAverageRoundDuration.php b/src/Domain/Round/IFSCAverageRoundDuration.php index 8a3629d..2e4c907 100644 --- a/src/Domain/Round/IFSCAverageRoundDuration.php +++ b/src/Domain/Round/IFSCAverageRoundDuration.php @@ -14,49 +14,42 @@ private const int DEFAULT_ROUND_DURATION = 90; private const array AVERAGE_DURATIONS = [ - 'men_boulder_final' => 120, - 'men_boulder_semi_final' => 120, - 'women_boulder_final' => 120, - 'women_boulder_semi_final' => 120, - 'lead_final' => 120, - 'speed_final' => 90, - 'lead_semi_final' => 150, - 'speed_qualification' => 90, - 'women_lead_boulder_semi_final' => 90, - 'men_lead_boulder_final' => 180, - 'women_lead_boulder_final' => 180, - 'men_lead_boulder_semi_final' => 180, - 'lead_boulder_final' => 240, - 'lead_boulder_semi_final' => 120, - 'women_men_boulder_final' => 180, - 'women_men_boulder_semi_final' => 150, - 'women_men_speed_final' => 60, - 'women_men_lead_final' => 120, - 'women_men_lead_semi_final' => 150, - 'paraclimbing_final' => 210, - 'men_speed_qualification' => 60, - 'women_speed_final' => 60, - 'boulder_final' => 210, - 'men_lead_final' => 60, - 'men_lead_semi_final' => 120, - 'women_lead_final' => 120, - 'women_lead_semi_final' => 210, - 'boulder_semi_final' => 120, - 'women_men_lead_boulder_semi_final_final' => 360, - 'combined_final' => 420, - 'lead_combined_qualification' => 90, - 'boulder_combined_qualification' => 120, - 'speed_combined_qualification' => 60, - 'men_combined_final' => 240, - 'women_combined_final' => 240, - 'women_boulder_combined_qualification' => 120, - 'men_lead_combined_qualification' => 90, - 'men_boulder_combined_qualification' => 120, - 'women_lead_combined_qualification' => 120, - 'women_final' => 120, - 'women_men_paraclimbing_final' => 150, - 'paraclimbing' => 120, - 'men_boulder_paraclimbing_final' => 150, + 'boulder_combined_men_qualification' => 120, + 'boulder_combined_men_qualification_women' => 120, + 'boulder_combined_qualification_women' => 120, + 'boulder_final_lead_men' => 180, + 'boulder_final_lead_men_semi_final_women' => 360, + 'boulder_final_lead_men_women' => 240, + 'boulder_final_lead_women' => 180, + 'boulder_final_men' => 120, + 'boulder_final_men_paraclimbing' => 150, + 'boulder_final_men_women' => 180, + 'boulder_final_women' => 120, + 'boulder_lead_men_semi_final' => 180, + 'boulder_lead_men_semi_final_women' => 120, + 'boulder_lead_semi_final_women' => 90, + 'boulder_men_semi_final' => 120, + 'boulder_men_semi_final_women' => 120, + 'boulder_semi_final_women' => 120, + 'combined_final_men' => 240, + 'combined_final_men_women' => 420, + 'combined_final_women' => 240, + 'combined_lead_men_qualification' => 90, + 'combined_lead_men_qualification_women' => 90, + 'combined_lead_qualification_women' => 120, + 'combined_men_qualification_speed_women' => 60, + 'final_lead_men' => 60, + 'final_lead_men_women' => 120, + 'final_lead_women' => 120, + 'final_men_paraclimbing_women' => 210, + 'final_men_speed_women' => 90, + 'final_speed_women' => 60, + 'lead_men_semi_final' => 120, + 'lead_men_semi_final_women' => 150, + 'lead_semi_final_women' => 210, + 'men_paraclimbing_women' => 120, + 'men_qualification_speed' => 60, + 'men_qualification_speed_women' => 90, ]; /** @param Tag[] $tags */ @@ -69,13 +62,15 @@ public function fromTags(array $tags): int /** @param Tag[] $tags */ private function lookupKey(array $tags): string { - $lookupKey = implode('_', - array_map( - static fn (Tag $tag): string => $tag->name, - $tags, - ) + $tags = array_map( + static fn (Tag $tag): string => $tag->name, + $tags, ); - return strtolower($lookupKey); + sort($tags); + + return strtolower( + implode('_', $tags), + ); } } diff --git a/src/Domain/Tags/IFSCParsedTags.php b/src/Domain/Tags/IFSCParsedTags.php index 3e1d760..9af9e56 100644 --- a/src/Domain/Tags/IFSCParsedTags.php +++ b/src/Domain/Tags/IFSCParsedTags.php @@ -98,7 +98,7 @@ public function isPreRound(): bool return $this->hasTag(Tag::PRE_ROUND); } - private function hasTag(Tag $tag): bool + public function hasTag(Tag $tag): bool { return in_array($tag, $this->tags, strict: true); } diff --git a/tests/unit/Domain/Round/IFSCRoundFactoryTest.php b/tests/unit/Domain/Round/IFSCRoundFactoryTest.php index bdeb712..b07a607 100644 --- a/tests/unit/Domain/Round/IFSCRoundFactoryTest.php +++ b/tests/unit/Domain/Round/IFSCRoundFactoryTest.php @@ -25,7 +25,7 @@ final class IFSCRoundFactoryTest extends TestCase { #[Test] public function finished_live_stream_start_and_end_time_is_used(): void { - $roundFactory = $this->roundFactoryWithLiveStreamWith( + $roundFactory = $this->ReturningLiveStreamWith( scheduledStartTime: '2024-04-12T10:55:00Z', duration: 60, ); @@ -45,7 +45,7 @@ final class IFSCRoundFactoryTest extends TestCase #[Test] public function average_round_duration_is_used_when_not_streamed_yet(): void { - $roundFactory = $this->roundFactoryWithLiveStreamWith( + $roundFactory = $this->ReturningLiveStreamWith( scheduledStartTime: '2024-04-12T10:55:00Z', duration: 0, ); @@ -65,7 +65,7 @@ final class IFSCRoundFactoryTest extends TestCase #[Test] public function round_schedule_is_not_confirmed_unless_a_live_stream_was_found(): void { - $roundFactory = $this->roundFactoryWithLiveStreamWith( + $roundFactory = $this->ReturningLiveStreamWith( scheduledStartTime: null, duration: 0, ); @@ -83,7 +83,7 @@ final class IFSCRoundFactoryTest extends TestCase $this->assertSame('2024-04-13T04:00:00+08:00', $this->formatDate($round->endTime)); } - private function roundFactoryWithLiveStreamWith(?string $scheduledStartTime, int $duration): IFSCRoundFactory + private function ReturningLiveStreamWith(?string $scheduledStartTime, int $duration): IFSCRoundFactory { return new IFSCRoundFactory( new IFSCTagsParser(), From 238575b859058a3ca727e676c2038dcb5a40cc59 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Sat, 13 Apr 2024 12:43:50 +0200 Subject: [PATCH 30/32] Guess end time base on YouTube's start time if times mismatch --- src/Domain/Round/IFSCAverageRoundDuration.php | 1 + src/Domain/Round/IFSCRoundFactory.php | 32 ++++++++++++++++--- .../Domain/Round/IFSCRoundFactoryTest.php | 20 ++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/Domain/Round/IFSCAverageRoundDuration.php b/src/Domain/Round/IFSCAverageRoundDuration.php index 2e4c907..324125c 100644 --- a/src/Domain/Round/IFSCAverageRoundDuration.php +++ b/src/Domain/Round/IFSCAverageRoundDuration.php @@ -13,6 +13,7 @@ { private const int DEFAULT_ROUND_DURATION = 90; + // Run `bin/avg-round-duration` to generate an updated list private const array AVERAGE_DURATIONS = [ 'boulder_combined_men_qualification' => 120, 'boulder_combined_men_qualification_women' => 120, diff --git a/src/Domain/Round/IFSCRoundFactory.php b/src/Domain/Round/IFSCRoundFactory.php index 5ac7837..d29b394 100644 --- a/src/Domain/Round/IFSCRoundFactory.php +++ b/src/Domain/Round/IFSCRoundFactory.php @@ -36,7 +36,14 @@ public function create( $liveStream = $this->findLiveStream($event, $roundName); if ($liveStream->scheduledStartTime) { - $startTime = $this->buildStartTime($liveStream, $event); + $youTubeStartTime = $this->buildStartTime($liveStream, $event); + + if ($endTime && !$this->startTimeMatchesYouTubes($startTime, $liveStream)) { + $diff = $startTime->diff($endTime); + $endTime = $youTubeStartTime->add($diff); + } + + $startTime = $youTubeStartTime; if ($liveStream->duration > 0) { $endTime = $startTime->modify( @@ -48,9 +55,7 @@ public function create( } if (!$endTime) { - $endTime = $startTime->modify( - sprintf('+%d minutes', $this->averageRoundDuration($tags)), - ); + $endTime = $this->calcEndTime($startTime, $tags); } return new IFSCRound( @@ -95,4 +100,23 @@ private function averageRoundDuration(IFSCParsedTags $tags): int { return $this->averageRoundDuration->fromTags($tags->allTags()); } + + private function startTimeMatchesYouTubes(DateTimeImmutable $startTime, LiveStream $liveStream): bool + { + $diff = $startTime->diff($liveStream->scheduledStartTime); + + return + $diff->i <= 5 && + $diff->y === 0 && + $diff->m === 0 && + $diff->d === 0 && + $diff->h === 0; + } + + private function calcEndTime(DateTimeImmutable $startTime, IFSCParsedTags $tags): DateTimeImmutable + { + return $startTime->modify( + sprintf('+%d minutes', $this->averageRoundDuration($tags)), + ); + } } diff --git a/tests/unit/Domain/Round/IFSCRoundFactoryTest.php b/tests/unit/Domain/Round/IFSCRoundFactoryTest.php index b07a607..54cfc9c 100644 --- a/tests/unit/Domain/Round/IFSCRoundFactoryTest.php +++ b/tests/unit/Domain/Round/IFSCRoundFactoryTest.php @@ -83,6 +83,26 @@ final class IFSCRoundFactoryTest extends TestCase $this->assertSame('2024-04-13T04:00:00+08:00', $this->formatDate($round->endTime)); } + #[Test] public function end_time_is_guessed_based_on_youtube_if_start_dates_mismatch(): void + { + $roundFactory = $this->ReturningLiveStreamWith( + scheduledStartTime: '2024-04-12T17:55:00+08:00', + duration: 0, + ); + + $round = $roundFactory->create( + event: $this->createEvent(), + roundName: "Men's & Women's Lead Qualification", + startTime: $this->createDateTime('2024-04-12T19:00:00+08:00'), + endTime: $this->createDateTime('2024-04-12T20:00:00+08:00'), + status: IFSCRoundStatus::PROVISIONAL, + ); + + $this->assertSame(IFSCRoundStatus::CONFIRMED, $round->status); + $this->assertSame('2024-04-12T18:00:00+08:00', $this->formatDate($round->startTime)); + $this->assertSame('2024-04-12T19:00:00+08:00', $this->formatDate($round->endTime)); + } + private function ReturningLiveStreamWith(?string $scheduledStartTime, int $duration): IFSCRoundFactory { return new IFSCRoundFactory( From 1c585c19f805c94951207a131186affbd6e98da3 Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Sat, 13 Apr 2024 12:48:33 +0200 Subject: [PATCH 31/32] Fix name --- tests/unit/Domain/Round/IFSCRoundFactoryTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/Domain/Round/IFSCRoundFactoryTest.php b/tests/unit/Domain/Round/IFSCRoundFactoryTest.php index 54cfc9c..d0c29c7 100644 --- a/tests/unit/Domain/Round/IFSCRoundFactoryTest.php +++ b/tests/unit/Domain/Round/IFSCRoundFactoryTest.php @@ -25,7 +25,7 @@ final class IFSCRoundFactoryTest extends TestCase { #[Test] public function finished_live_stream_start_and_end_time_is_used(): void { - $roundFactory = $this->ReturningLiveStreamWith( + $roundFactory = $this->roundFactoryReturningLiveStreamWith( scheduledStartTime: '2024-04-12T10:55:00Z', duration: 60, ); @@ -45,7 +45,7 @@ final class IFSCRoundFactoryTest extends TestCase #[Test] public function average_round_duration_is_used_when_not_streamed_yet(): void { - $roundFactory = $this->ReturningLiveStreamWith( + $roundFactory = $this->roundFactoryReturningLiveStreamWith( scheduledStartTime: '2024-04-12T10:55:00Z', duration: 0, ); @@ -65,7 +65,7 @@ final class IFSCRoundFactoryTest extends TestCase #[Test] public function round_schedule_is_not_confirmed_unless_a_live_stream_was_found(): void { - $roundFactory = $this->ReturningLiveStreamWith( + $roundFactory = $this->roundFactoryReturningLiveStreamWith( scheduledStartTime: null, duration: 0, ); @@ -85,7 +85,7 @@ final class IFSCRoundFactoryTest extends TestCase #[Test] public function end_time_is_guessed_based_on_youtube_if_start_dates_mismatch(): void { - $roundFactory = $this->ReturningLiveStreamWith( + $roundFactory = $this->roundFactoryReturningLiveStreamWith( scheduledStartTime: '2024-04-12T17:55:00+08:00', duration: 0, ); @@ -103,7 +103,7 @@ final class IFSCRoundFactoryTest extends TestCase $this->assertSame('2024-04-12T19:00:00+08:00', $this->formatDate($round->endTime)); } - private function ReturningLiveStreamWith(?string $scheduledStartTime, int $duration): IFSCRoundFactory + private function roundFactoryReturningLiveStreamWith(?string $scheduledStartTime, int $duration): IFSCRoundFactory { return new IFSCRoundFactory( new IFSCTagsParser(), From 2d99596ebe1b00161f19ac09c410d81ceac245ff Mon Sep 17 00:00:00 2001 From: Nico Oelgart Date: Sat, 13 Apr 2024 12:52:08 +0200 Subject: [PATCH 32/32] Small refactor --- src/Domain/Round/IFSCRoundFactory.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Domain/Round/IFSCRoundFactory.php b/src/Domain/Round/IFSCRoundFactory.php index d29b394..6e999db 100644 --- a/src/Domain/Round/IFSCRoundFactory.php +++ b/src/Domain/Round/IFSCRoundFactory.php @@ -107,10 +107,7 @@ private function startTimeMatchesYouTubes(DateTimeImmutable $startTime, LiveStre return $diff->i <= 5 && - $diff->y === 0 && - $diff->m === 0 && - $diff->d === 0 && - $diff->h === 0; + ($diff->y + $diff->m + $diff->d + $diff->h) === 0; } private function calcEndTime(DateTimeImmutable $startTime, IFSCParsedTags $tags): DateTimeImmutable