diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index fc03d9e..b485f73 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -48,7 +48,6 @@ class ElementController 'description' => $element->getDescription(), 'richText' => $element->getRichText(), 'pdfPath' => $element->getPdfPath(), - 'youtubeUrl' => $element->getYoutubeUrl(), ], ], 200); } diff --git a/backend/app/Element/CreateElementDto.php b/backend/app/Element/CreateElementDto.php index cd8bec2..e5cc6fe 100644 --- a/backend/app/Element/CreateElementDto.php +++ b/backend/app/Element/CreateElementDto.php @@ -12,7 +12,6 @@ class CreateElementDto public string $description, public string $richText, public ?string $pdfPath, - public ?string $youtubeUrl, public ?Element $parentElement, ) { } diff --git a/backend/app/Element/Element.php b/backend/app/Element/Element.php index f747d12..f8e9f38 100644 --- a/backend/app/Element/Element.php +++ b/backend/app/Element/Element.php @@ -12,7 +12,6 @@ class Element private string $description, private string $richText, private ?string $pdfPath, - private ?string $youtubeUrl, private Set $set, private ?Element $parentElement, ) { @@ -43,11 +42,6 @@ class Element return $this->pdfPath; } - public function getYoutubeUrl(): ?string - { - return $this->youtubeUrl; - } - public function getSet(): Set { return $this->set; diff --git a/backend/app/Element/ElementModel.php b/backend/app/Element/ElementModel.php index b246351..2655d04 100644 --- a/backend/app/Element/ElementModel.php +++ b/backend/app/Element/ElementModel.php @@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Model; * @property string $description * @property string $rich_text * @property string|null $pdf_path - * @property string|null $youtube_url * @property int|null $parent_element_id * * @method static Builder|ElementModel newModelQuery() @@ -25,7 +24,6 @@ use Illuminate\Database\Eloquent\Model; * @method static Builder|ElementModel whereDescription($value) * @method static Builder|ElementModel whereRichText($value) * @method static Builder|ElementModel wherePdfPath($value) - * @method static Builder|ElementModel whereYoutubeUrl($value) * * @mixin \Eloquent */ @@ -41,7 +39,6 @@ class ElementModel extends Model 'description', 'rich_text', 'pdf_path', - 'youtube_url', 'parent_element_id', ]; diff --git a/backend/app/Element/EloquentElementRepository.php b/backend/app/Element/EloquentElementRepository.php index a957880..389d37b 100644 --- a/backend/app/Element/EloquentElementRepository.php +++ b/backend/app/Element/EloquentElementRepository.php @@ -20,7 +20,6 @@ class EloquentElementRepository implements ElementRepository 'description' => $dto->description, 'rich_text' => $dto->richText, 'pdf_path' => $dto->pdfPath, - 'youtube_url' => $dto->youtubeUrl, 'parent_element_id' => $dto->parentElement?->getId(), ]); @@ -30,7 +29,6 @@ class EloquentElementRepository implements ElementRepository description: $dto->description, richText: $dto->richText, pdfPath: $dto->pdfPath, - youtubeUrl: $dto->youtubeUrl, set: $dto->set, parentElement: $dto->parentElement, ); @@ -113,7 +111,6 @@ class EloquentElementRepository implements ElementRepository description: $model->description, richText: $model->rich_text, pdfPath: $model->pdf_path, - youtubeUrl: $model->youtube_url, set: $set, parentElement: $parentElement, ); diff --git a/backend/app/Element/UseCases/CreateElement/CreateElement.php b/backend/app/Element/UseCases/CreateElement/CreateElement.php index c0a8661..364ef0a 100644 --- a/backend/app/Element/UseCases/CreateElement/CreateElement.php +++ b/backend/app/Element/UseCases/CreateElement/CreateElement.php @@ -33,9 +33,6 @@ class CreateElement $description = $request->description ?? ''; $richText = $request->richText ?? ''; $pdfPath = $request->pdfPath === '' ? null : $request->pdfPath; - $youtubeUrl = $request->youtubeUrl === '' - ? null - : $request->youtubeUrl; $set = $this->setRepo->find($request->setId); if ($set === null) { @@ -53,7 +50,6 @@ class CreateElement description: $description, richText: $richText, pdfPath: $pdfPath, - youtubeUrl: $youtubeUrl, parentElement: null, )); } @@ -78,7 +74,6 @@ class CreateElement description: $description, richText: $richText, pdfPath: $pdfPath, - youtubeUrl: $youtubeUrl, parentElement: $parentElement, )); } diff --git a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php index 1738838..b63628a 100644 --- a/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php +++ b/backend/app/Element/UseCases/CreateElement/CreateElementRequest.php @@ -10,7 +10,6 @@ class CreateElementRequest public ?string $description, public ?string $richText, public ?string $pdfPath, - public ?string $youtubeUrl, public ?int $parentElementId, ) { } diff --git a/backend/database/migrations/2026_05_24_000001_elements_table.php b/backend/database/migrations/2026_05_24_000001_elements_table.php index 90f708c..c0edefa 100644 --- a/backend/database/migrations/2026_05_24_000001_elements_table.php +++ b/backend/database/migrations/2026_05_24_000001_elements_table.php @@ -15,7 +15,6 @@ return new class extends Migration $table->text('description')->default(''); $table->text('rich_text')->default(''); $table->string('pdf_path')->nullable(); - $table->string('youtube_url')->nullable(); $table->foreignId('parent_element_id') ->nullable() ->constrained('elements'); diff --git a/backend/database/seeders/ElementSeeder.php b/backend/database/seeders/ElementSeeder.php index af7ff09..592396d 100644 --- a/backend/database/seeders/ElementSeeder.php +++ b/backend/database/seeders/ElementSeeder.php @@ -22,8 +22,6 @@ class ElementSeeder extends Seeder . '

Move steadily from awareness ' . 'to practice.

', pdfPath: '/assets/pdfs/baderech.pdf', - youtubeUrl: 'https://www.youtube.com/watch?v=' - . 'yHx-r4p6hHU&t=1s', parentElement: null, )); $elementRepository->create(new CreateElementDto( @@ -34,7 +32,6 @@ class ElementSeeder extends Seeder richText: '

Avodah foundations begin with honest awareness ' . 'and small repeatable steps.

', pdfPath: null, - youtubeUrl: null, parentElement: $rootElement, )); $elementRepository->create(new CreateElementDto( @@ -44,7 +41,6 @@ class ElementSeeder extends Seeder richText: '

Daily practice turns inspiration into a ' . 'dependable rhythm.

', pdfPath: null, - youtubeUrl: null, parentElement: $rootElement, )); } diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php index a877d8b..0ccdfeb 100644 --- a/backend/tests/Fakes/FakeElementRepository.php +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -23,7 +23,6 @@ class FakeElementRepository implements ElementRepository description: $dto->description, richText: $dto->richText, pdfPath: $dto->pdfPath, - youtubeUrl: $dto->youtubeUrl, set: $dto->set, parentElement: $dto->parentElement, ); @@ -102,7 +101,6 @@ class FakeElementRepository implements ElementRepository description: $element->getDescription(), richText: $element->getRichText(), pdfPath: $element->getPdfPath(), - youtubeUrl: $element->getYoutubeUrl(), set: $element->getSet(), parentElement: $parentElement, ); diff --git a/backend/tests/Feature/ElementsEndpointTest.php b/backend/tests/Feature/ElementsEndpointTest.php index 32b9f21..a24a6c8 100644 --- a/backend/tests/Feature/ElementsEndpointTest.php +++ b/backend/tests/Feature/ElementsEndpointTest.php @@ -15,8 +15,6 @@ class ElementsEndpointTest extends TestCase public function testReturnsElementTitle(): void { - $sampleYoutubeUrl = 'https://www.youtube.com/watch?v=' - . 'yHx-r4p6hHU&t=1s'; $setRepository = app(SetRepository::class); $elementRepository = app(ElementRepository::class); $set = $setRepository->create(new CreateSetDto( @@ -30,7 +28,6 @@ class ElementsEndpointTest extends TestCase description: 'A structured path for growth', richText: '

A structured path for growth

', pdfPath: '/assets/pdfs/baderech.pdf', - youtubeUrl: $sampleYoutubeUrl, parentElement: null, )); $firstChildElement = $elementRepository->create(new CreateElementDto( @@ -39,7 +36,6 @@ class ElementsEndpointTest extends TestCase description: 'Foundations for steady avodah', richText: '

Foundations rich text

', pdfPath: '/assets/pdfs/foundations.pdf', - youtubeUrl: null, parentElement: $element, )); $secondChildElement = $elementRepository->create(new CreateElementDto( @@ -48,7 +44,6 @@ class ElementsEndpointTest extends TestCase description: 'Daily practices for growth', richText: '

Daily practice rich text

', pdfPath: null, - youtubeUrl: null, parentElement: $element, )); @@ -74,7 +69,6 @@ class ElementsEndpointTest extends TestCase 'description' => 'A structured path for growth', 'richText' => '

A structured path for growth

', 'pdfPath' => '/assets/pdfs/baderech.pdf', - 'youtubeUrl' => $sampleYoutubeUrl, ], ]); } diff --git a/backend/tests/Feature/SetsEndpointTest.php b/backend/tests/Feature/SetsEndpointTest.php index e5cfdad..ed0bbca 100644 --- a/backend/tests/Feature/SetsEndpointTest.php +++ b/backend/tests/Feature/SetsEndpointTest.php @@ -34,7 +34,6 @@ class SetsEndpointTest extends TestCase description: $baderechSet->getDescription(), richText: '', pdfPath: null, - youtubeUrl: null, parentElement: null, ) ); diff --git a/backend/tests/Unit/Controllers/ElementControllerTest.php b/backend/tests/Unit/Controllers/ElementControllerTest.php index 95ba6f5..d9ffe2f 100644 --- a/backend/tests/Unit/Controllers/ElementControllerTest.php +++ b/backend/tests/Unit/Controllers/ElementControllerTest.php @@ -32,7 +32,6 @@ class ElementControllerTest extends TestCase 'A structured path for growth', '

A structured path for growth

', '/assets/pdfs/baderech.pdf', - 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', null, ); $firstChildElement = $this->createElement( @@ -41,7 +40,6 @@ class ElementControllerTest extends TestCase 'Foundations for steady avodah', '

Foundations rich text

', '/assets/pdfs/foundations.pdf', - null, $element, ); $secondChildElement = $this->createElement( @@ -50,7 +48,6 @@ class ElementControllerTest extends TestCase 'Daily practices for growth', '

Daily practice rich text

', null, - null, $element, ); @@ -72,10 +69,6 @@ class ElementControllerTest extends TestCase '/assets/pdfs/baderech.pdf', $body['element']['pdfPath'], ); - $this->assertSame( - 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', - $body['element']['youtubeUrl'], - ); $this->assertSame([ [ 'id' => $firstChildElement->getId(), @@ -128,7 +121,6 @@ class ElementControllerTest extends TestCase string $description, string $richText, ?string $pdfPath, - ?string $youtubeUrl, ?Element $parentElement, ): Element { return $this->elementRepo->create(new CreateElementDto( @@ -137,7 +129,6 @@ class ElementControllerTest extends TestCase description: $description, richText: $richText, pdfPath: $pdfPath, - youtubeUrl: $youtubeUrl, parentElement: $parentElement, )); } diff --git a/backend/tests/Unit/Element/ElementTest.php b/backend/tests/Unit/Element/ElementTest.php index 7d0bcf2..7808344 100644 --- a/backend/tests/Unit/Element/ElementTest.php +++ b/backend/tests/Unit/Element/ElementTest.php @@ -22,7 +22,6 @@ class ElementTest extends TestCase description: 'Root description', richText: '

Root rich text

', pdfPath: null, - youtubeUrl: null, set: $set, parentElement: null, ); @@ -32,7 +31,6 @@ class ElementTest extends TestCase description: 'Child description', richText: '

Child rich text

', pdfPath: '/assets/pdfs/child.pdf', - youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', set: $set, parentElement: $rootElement, ); @@ -51,14 +49,9 @@ class ElementTest extends TestCase '/assets/pdfs/child.pdf', $childElement->getPdfPath(), ); - $this->assertSame( - 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', - $childElement->getYoutubeUrl(), - ); $this->assertSame($set, $childElement->getSet()); $this->assertSame($rootElement, $childElement->getParentElement()); $this->assertNull($rootElement->getPdfPath()); - $this->assertNull($rootElement->getYoutubeUrl()); $this->assertNull($rootElement->getParentElement()); } } diff --git a/backend/tests/Unit/Element/UseCases/CreateElementTest.php b/backend/tests/Unit/Element/UseCases/CreateElementTest.php index 05e7d66..2b34873 100644 --- a/backend/tests/Unit/Element/UseCases/CreateElementTest.php +++ b/backend/tests/Unit/Element/UseCases/CreateElementTest.php @@ -50,7 +50,6 @@ class CreateElementTest extends TestCase description: 'Root description', richText: '

Root rich text

', pdfPath: '/assets/pdfs/root.pdf', - youtubeUrl: 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', parentElementId: null, )); @@ -59,10 +58,6 @@ class CreateElementTest extends TestCase $this->assertSame('Root description', $element->getDescription()); $this->assertSame('

Root rich text

', $element->getRichText()); $this->assertSame('/assets/pdfs/root.pdf', $element->getPdfPath()); - $this->assertSame( - 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', - $element->getYoutubeUrl(), - ); $this->assertSame($set->getId(), $element->getSet()->getId()); $this->assertNull($element->getParentElement()); } @@ -77,7 +72,6 @@ class CreateElementTest extends TestCase description: 'Root description', richText: '

Root rich text

', pdfPath: null, - youtubeUrl: null, parentElementId: null, ) ); @@ -89,7 +83,6 @@ class CreateElementTest extends TestCase description: 'Child description', richText: '

Child rich text

', pdfPath: '/assets/pdfs/child.pdf', - youtubeUrl: 'https://youtu.be/yHx-r4p6hHU', parentElementId: $rootElement->getId(), ) ); @@ -107,10 +100,6 @@ class CreateElementTest extends TestCase '/assets/pdfs/child.pdf', $childElement->getPdfPath(), ); - $this->assertSame( - 'https://youtu.be/yHx-r4p6hHU', - $childElement->getYoutubeUrl(), - ); $this->assertSame( $rootElement->getId(), $childElement->getParentElement()->getId(), @@ -127,14 +116,12 @@ class CreateElementTest extends TestCase description: null, richText: null, pdfPath: null, - youtubeUrl: null, parentElementId: null, )); $this->assertSame('', $element->getDescription()); $this->assertSame('', $element->getRichText()); $this->assertNull($element->getPdfPath()); - $this->assertNull($element->getYoutubeUrl()); } public function testCreatesElementWithNullPdfPathWhenBlank(): void @@ -147,30 +134,12 @@ class CreateElementTest extends TestCase description: 'Root description', richText: '

Root rich text

', pdfPath: '', - youtubeUrl: null, parentElementId: null, )); $this->assertNull($element->getPdfPath()); } - public function testCreatesElementWithNullYoutubeUrlWhenBlank(): void - { - $set = $this->createSet('Daily learning'); - - $element = $this->createElement->execute(new CreateElementRequest( - setId: $set->getId(), - title: 'Root', - description: 'Root description', - richText: '

Root rich text

', - pdfPath: null, - youtubeUrl: '', - parentElementId: null, - )); - - $this->assertNull($element->getYoutubeUrl()); - } - public function testThrowsWhenSetIdMissing(): void { $this->expectException(BadRequestException::class); @@ -182,7 +151,6 @@ class CreateElementTest extends TestCase description: 'Root description', richText: '

Root rich text

', pdfPath: null, - youtubeUrl: null, parentElementId: null, )); } @@ -198,7 +166,6 @@ class CreateElementTest extends TestCase description: 'Root description', richText: '

Root rich text

', pdfPath: null, - youtubeUrl: null, parentElementId: null, )); } @@ -214,7 +181,6 @@ class CreateElementTest extends TestCase description: 'Root description', richText: '

Root rich text

', pdfPath: null, - youtubeUrl: null, parentElementId: null, )); } @@ -234,7 +200,6 @@ class CreateElementTest extends TestCase description: 'Child description', richText: '

Child rich text

', pdfPath: null, - youtubeUrl: null, parentElementId: 99, )); } @@ -248,7 +213,6 @@ class CreateElementTest extends TestCase description: 'Root description', richText: '

Root rich text

', pdfPath: null, - youtubeUrl: null, parentElementId: null, )); @@ -263,7 +227,6 @@ class CreateElementTest extends TestCase description: 'Another root description', richText: '

Another root rich text

', pdfPath: null, - youtubeUrl: null, parentElementId: null, )); } @@ -279,7 +242,6 @@ class CreateElementTest extends TestCase description: 'Parent root description', richText: '

Parent root rich text

', pdfPath: null, - youtubeUrl: null, parentElementId: null, ) ); @@ -295,7 +257,6 @@ class CreateElementTest extends TestCase description: 'Invalid child description', richText: '

Invalid child rich text

', pdfPath: null, - youtubeUrl: null, parentElementId: $parentElement->getId(), )); } diff --git a/backend/tests/Unit/Element/UseCases/GetElementTest.php b/backend/tests/Unit/Element/UseCases/GetElementTest.php index f175b62..2b11d63 100644 --- a/backend/tests/Unit/Element/UseCases/GetElementTest.php +++ b/backend/tests/Unit/Element/UseCases/GetElementTest.php @@ -33,7 +33,6 @@ class GetElementTest extends TestCase 'A structured path for growth', '

A structured path for growth

', '/assets/pdfs/baderech.pdf', - 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', null, ); @@ -57,10 +56,6 @@ class GetElementTest extends TestCase '/assets/pdfs/baderech.pdf', $foundElement->getPdfPath(), ); - $this->assertSame( - 'https://www.youtube.com/watch?v=yHx-r4p6hHU&t=1s', - $foundElement->getYoutubeUrl(), - ); } public function testReturnsDirectChildElements(): void @@ -73,7 +68,6 @@ class GetElementTest extends TestCase '

A structured path for growth

', '/assets/pdfs/baderech.pdf', null, - null, ); $firstChildElement = $this->createElement( $set, @@ -81,7 +75,6 @@ class GetElementTest extends TestCase 'Foundations for steady avodah', '

Foundations rich text

', '/assets/pdfs/foundations.pdf', - null, $parentElement, ); $secondChildElement = $this->createElement( @@ -90,7 +83,6 @@ class GetElementTest extends TestCase 'Daily practices for growth', '

Daily practice rich text

', null, - null, $parentElement, ); $this->createElement( @@ -99,7 +91,6 @@ class GetElementTest extends TestCase 'Nested description', '

Nested rich text

', null, - null, $firstChildElement, ); $otherSet = $this->createSet(2, 'Daily Learning'); @@ -110,7 +101,6 @@ class GetElementTest extends TestCase '

Other parent rich text

', null, null, - null, ); $this->createElement( $otherSet, @@ -118,7 +108,6 @@ class GetElementTest extends TestCase 'Other child description', '

Other child rich text

', null, - null, $otherParentElement, ); @@ -180,7 +169,6 @@ class GetElementTest extends TestCase string $description, string $richText, ?string $pdfPath, - ?string $youtubeUrl, ?Element $parentElement, ): Element { return $this->elementRepo->create(new CreateElementDto( @@ -189,7 +177,6 @@ class GetElementTest extends TestCase description: $description, richText: $richText, pdfPath: $pdfPath, - youtubeUrl: $youtubeUrl, parentElement: $parentElement, )); } diff --git a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts index daa0532..5901c8e 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/media.cy.ts @@ -46,11 +46,6 @@ describe('media page sets', () => { .should('be.visible') cy.contains('strong', 'Move steadily').should('be.visible') }) - cy.get('[data-cy="element-youtube-embed"]') - .should('be.visible') - .and('have.attr', 'src') - .and('include', 'https://www.youtube.com/embed/yHx-r4p6hHU') - .and('include', 'start=1') cy.contains('[data-cy="element-pdf-link"]', 'View PDF') .should('be.visible') .and('have.attr', 'href', '/assets/pdfs/baderech.pdf') @@ -87,7 +82,6 @@ describe('media page sets', () => { 'contain.text', 'Avodah foundations begin with honest awareness', ) - cy.get('[data-cy="element-youtube-embed"]').should('not.exist') cy.get('[data-cy="element-pdf-link"]').should('not.exist') }) }) diff --git a/frontend/rabbi_gerzi/src/stores/elements.ts b/frontend/rabbi_gerzi/src/stores/elements.ts index 5b52fdd..eb82ffc 100644 --- a/frontend/rabbi_gerzi/src/stores/elements.ts +++ b/frontend/rabbi_gerzi/src/stores/elements.ts @@ -10,7 +10,6 @@ export interface ChildElement { export interface Element extends ChildElement { richText: string pdfPath: string | null - youtubeUrl: string | null } interface ElementResponse { diff --git a/frontend/rabbi_gerzi/src/views/ElementPage.vue b/frontend/rabbi_gerzi/src/views/ElementPage.vue index cee7153..10fdbbf 100644 --- a/frontend/rabbi_gerzi/src/views/ElementPage.vue +++ b/frontend/rabbi_gerzi/src/views/ElementPage.vue @@ -5,22 +5,10 @@ import { useRoute } from 'vue-router' import SiteHeader from '@/components/SiteHeader.vue' import { useElementsStore } from '@/stores/elements' -type TimestampPart = string | undefined - const route = useRoute() const elementsStore = useElementsStore() const { element, childElements, isLoading, error } = storeToRefs(elementsStore) -const youtubeIframeAllow = [ - 'accelerometer', - 'autoplay', - 'clipboard-write', - 'encrypted-media', - 'gyroscope', - 'picture-in-picture', - 'web-share', -].join('; ') - const elementId = computed(() => { const routeElementId = route.params.id @@ -31,12 +19,6 @@ const elementId = computed(() => { return routeElementId }) -const youtubeEmbedUrl = computed(() => { - const youtubeUrl = element.value?.youtubeUrl ?? null - - return getYoutubeEmbedUrl(youtubeUrl) -}) - watch( elementId, (currentElementId) => { @@ -48,154 +30,6 @@ watch( }, { immediate: true }, ) - -function getYoutubeEmbedUrl(youtubeUrl: string | null): string | null { - if (youtubeUrl === null || youtubeUrl === '') { - return null - } - - let parsedUrl: URL - try { - parsedUrl = new URL(youtubeUrl) - } catch { - return null - } - - const videoId = getYoutubeVideoId(parsedUrl) - if (videoId === null) { - return null - } - - const embedUrl = new URL(`https://www.youtube.com/embed/${videoId}`) - const startSeconds = getYoutubeStartSeconds(parsedUrl) - if (startSeconds !== null) { - embedUrl.searchParams.set('start', startSeconds.toString()) - } - - return embedUrl.toString() -} - -function getYoutubeVideoId(parsedUrl: URL): string | null { - if (isShortYoutubeHost(parsedUrl.hostname)) { - return normalizeYoutubeVideoId(getFirstPathSegment(parsedUrl)) - } - - if (!isYoutubeHost(parsedUrl.hostname)) { - return null - } - - if (parsedUrl.pathname === '/watch') { - return normalizeYoutubeVideoId(parsedUrl.searchParams.get('v')) - } - - if (parsedUrl.pathname.startsWith('/embed/')) { - return normalizeYoutubeVideoId(getPathSegment(parsedUrl, 1)) - } - - return null -} - -function getYoutubeStartSeconds(parsedUrl: URL): number | null { - const startParam = parsedUrl.searchParams.get('start') - if (startParam !== null) { - return getPositiveSeconds(startParam) - } - - const timestampParam = parsedUrl.searchParams.get('t') - if (timestampParam === null) { - return null - } - - return getTimestampSeconds(timestampParam) -} - -function getTimestampSeconds(timestampParam: string): number | null { - if (/^\d+$/.test(timestampParam)) { - return getPositiveSeconds(timestampParam) - } - - const hourRegex = '^(?:(\\d+)h)?' - const minuteRegex = '(?:(\\d+)m)?' - const secondRegex = '(?:(\\d+)s)?$' - const fullTimestampRegex = hourRegex + minuteRegex + secondRegex - const timestampPattern = new RegExp(fullTimestampRegex) - const timestampMatch = timestampParam.match(timestampPattern) - if (timestampMatch === null) { - return null - } - - const hours = getTimestampHours(timestampMatch[1]) - const minutes = getTimestampMinutes(timestampMatch[2]) - const seconds = getTimestampSecondsPart(timestampMatch[3]) - const totalSeconds = hours + minutes + seconds - - return totalSeconds > 0 ? totalSeconds : null -} - -function getTimestampHours(timestampPart: TimestampPart): number { - return getPartSeconds(timestampPart, 3600) -} - -function getTimestampMinutes(timestampPart: TimestampPart): number { - return getPartSeconds(timestampPart, 60) -} - -function getTimestampSecondsPart(timestampPart: TimestampPart): number { - return getPartSeconds(timestampPart, 1) -} - -function getPartSeconds(timestamp: TimestampPart, multiplier: number): number { - if (timestamp === undefined) { - return 0 - } - - return Number(timestamp) * multiplier -} - -function getPositiveSeconds(secondsParam: string): number | null { - const seconds = Number(secondsParam) - if (!Number.isInteger(seconds) || seconds < 0) { - return null - } - - return seconds -} - -function getFirstPathSegment(parsedUrl: URL): string | null { - return getPathSegment(parsedUrl, 0) -} - -function getPathSegment(parsedUrl: URL, segmentIndex: number): string | null { - const pathSegments = parsedUrl.pathname.split('/').filter((pathSegment) => { - return pathSegment !== '' - }) - - return pathSegments[segmentIndex] ?? null -} - -function normalizeYoutubeVideoId(videoId: string | null): string | null { - if (videoId === null || videoId === '') { - return null - } - - if (!/^[A-Za-z0-9_-]+$/.test(videoId)) { - return null - } - - return videoId -} - -function isYoutubeHost(hostname: string): boolean { - const normalizedHostname = hostname.toLowerCase() - const isRootYoutubeHost = normalizedHostname === 'youtube.com' - const isYoutubeSubdomain = normalizedHostname.endsWith('.youtube.com') - - return isRootYoutubeHost || isYoutubeSubdomain -} - -function isShortYoutubeHost(hostname: string): boolean { - return hostname.toLowerCase() === 'youtu.be' -}