From 8e1b623452f443bff2f13796f06195a57737a86d Mon Sep 17 00:00:00 2001 From: Nils Knappmeier Date: Sun, 18 Aug 2024 23:20:31 +0200 Subject: [PATCH] create videopack and editor --- package-lock.json | 31 + package.json | 1 + scripts/buildVideoPack.ts | 37 + scripts/sort-translations.ts | 6 +- src/YoutubePlayer/adapter.ts | 48 + src/YoutubePlayer/index.ts | 22 +- .../solid/organisms/Reader/Reader.tsx | 8 +- .../VideoPackEditor/VideoPackEditor.tsx | 228 ++ .../solid/organisms/VideoPackEditor/index.ts | 1 + src/core/buildExamTable/buildExamTable.ts | 30 +- src/core/model/Exam.ts | 10 +- src/core/model/TechniqueMetadata.ts | 3 + src/core/model/TechniqueTree.ts | 9 + src/core/model/VideoPack.ts | 8 + src/core/model/index.ts | 1 + .../resolveExamTables/resolveExamTables.ts | 20 +- src/core/utils/coerceToArray.test.ts | 16 + src/core/utils/coerceToArray.ts | 9 + .../videopacks/aikido-kompendium/index.ts | 2572 +++++++++++++++++ src/data/videopacks/index.test.ts | 11 + src/data/videopacks/index.ts | 26 + src/i18n/common/de.json | 1 + src/i18n/common/en.json | 1 + .../videos/[videoPackId]/index.astro | 30 + src/pages/[language]/videos/index.astro | 29 + 25 files changed, 3111 insertions(+), 47 deletions(-) create mode 100644 scripts/buildVideoPack.ts create mode 100644 src/YoutubePlayer/adapter.ts create mode 100644 src/components/solid/organisms/VideoPackEditor/VideoPackEditor.tsx create mode 100644 src/components/solid/organisms/VideoPackEditor/index.ts create mode 100644 src/core/model/TechniqueTree.ts create mode 100644 src/core/model/VideoPack.ts create mode 100644 src/core/utils/coerceToArray.test.ts create mode 100644 src/core/utils/coerceToArray.ts create mode 100644 src/data/videopacks/aikido-kompendium/index.ts create mode 100644 src/data/videopacks/index.test.ts create mode 100644 src/data/videopacks/index.ts create mode 100644 src/pages/[language]/videos/[videoPackId]/index.astro create mode 100644 src/pages/[language]/videos/index.astro diff --git a/package-lock.json b/package-lock.json index 068dae1..0203874 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@astrojs/tailwind": "5.1.0", "@material-design-icons/svg": "0.14.13", "@nanostores/persistent": "0.10.2", + "@tanstack/solid-table": "8.20.1", "@testing-library/jest-dom": "6.4.8", "@testing-library/react": "16.0.0", "astro-i18next": "1.0.0-beta.21", @@ -2275,6 +2276,36 @@ "@types/hast": "^3.0.4" } }, + "node_modules/@tanstack/solid-table": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@tanstack/solid-table/-/solid-table-8.20.1.tgz", + "integrity": "sha512-W5MdWxFL7HHmxXKyl2WnoikAcp91d6nIXUodd0a20TWKj4QiwERhDcx0Y9sSIdchOuybxROyopBkSvFj2iyZLA==", + "dependencies": { + "@tanstack/table-core": "8.20.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "solid-js": ">=1.3" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.1.tgz", + "integrity": "sha512-5Ly5TIRHnWH7vSDell9B/OVyV380qqIJVg7H7R7jU4fPEmOD4smqAX7VRflpYI09srWR8aj5OLD2Ccs1pI5mTg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", diff --git a/package.json b/package.json index 88d2b16..225e0ae 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@astrojs/tailwind": "5.1.0", "@material-design-icons/svg": "0.14.13", "@nanostores/persistent": "0.10.2", + "@tanstack/solid-table": "8.20.1", "@testing-library/jest-dom": "6.4.8", "@testing-library/react": "16.0.0", "astro-i18next": "1.0.0-beta.21", diff --git a/scripts/buildVideoPack.ts b/scripts/buildVideoPack.ts new file mode 100644 index 0000000..37cb7c2 --- /dev/null +++ b/scripts/buildVideoPack.ts @@ -0,0 +1,37 @@ +/** + * Just a one time script to generate a video pack. This may be removed in the future. + */ + +import darmstadt from "../src/data/dojos/aikido-dojo-darmstadt/details.ts"; +import { resolveExamTables } from "$core/resolveExamTables"; +import { buildTechniqueTree } from "$core/buildExamTable/buildExamTable.ts"; +import type { VideoPack } from "$core/model/VideoPack.ts"; +import { coerceToArray } from "$core/utils/coerceToArray.ts"; + +import prettier from "prettier"; + +const allTechniques = resolveExamTables(darmstadt.exams); + +let counter = 0; +for (const technique of allTechniques) { + for (const youtube of coerceToArray(technique.metadata.youtube)) { + youtube.id = String(counter++); + } +} + +const pack: VideoPack = { + name: "Aikido Kompendium", + source: "https://www.aikido-kompendium.de", + videos: buildTechniqueTree(allTechniques, (technique) => coerceToArray(technique.metadata.youtube)), +}; + +// eslint-disable-next-line no-console +console.log( + await prettier.format( + `import type {VideoPack} from "$core/model/VideoPack"; + +export default ${JSON.stringify(pack, null, 2)} satisfies VideoPack +`, + { parser: "typescript" }, + ), +); diff --git a/scripts/sort-translations.ts b/scripts/sort-translations.ts index e65b750..bb05c81 100644 --- a/scripts/sort-translations.ts +++ b/scripts/sort-translations.ts @@ -1,7 +1,11 @@ import fs from "node:fs/promises"; import prettier from "prettier"; +import { fileURLToPath } from "url"; -for (const file of process.argv.slice(2)) { +const urls = import.meta.glob("../src/i18n/**/*.json", { query: "url", eager: true, import: "default" }); +const files = Object.keys(urls).map((file) => fileURLToPath(new URL(file, import.meta.url))); + +for (const file of files) { const translations = JSON.parse(await fs.readFile(file, "utf-8")); const sortedTranslations = Object.fromEntries( Object.entries(translations).toSorted((a, b) => { diff --git a/src/YoutubePlayer/adapter.ts b/src/YoutubePlayer/adapter.ts new file mode 100644 index 0000000..69a5a27 --- /dev/null +++ b/src/YoutubePlayer/adapter.ts @@ -0,0 +1,48 @@ +import { youtubeEnabled } from "$core/store/youtube.ts"; + +export interface YoutubeAdapter { + loadVideoById(videoId: string, startSeconds?: number, endSeconds?: number): Promise; + playVideo(): Promise; + stopVideo(): Promise; + setSize(width: number, height: number): Promise; + waitForStop(): Promise; + getCurrentTime(): Promise; + destroy(): Promise; +} + +export async function loadYoutubeAdapter(container: HTMLDivElement): Promise { + if (!youtubeEnabled.get()) { + throw new Error("No youtube consent was given."); + } + const { default: Player } = await import("youtube-player"); + const player = Player(container, { + host: "https://www.youtube-nocookie.com", + playerVars: { + rel: 0, + autoplay: 0, + modestbranding: 1, + controls: 1, + }, + }); + + return { + playVideo: player.playVideo, + loadVideoById(videoId, startSeconds, endSeconds) { + return player.loadVideoById({ videoId, startSeconds, endSeconds }); + }, + stopVideo: player.stopVideo, + setSize: player.setSize, + waitForStop() { + return new Promise((resolve) => { + player.on("stateChange", (event) => { + if (event.data === 0) { + // Video has ended + resolve(); + } + }); + }); + }, + getCurrentTime: player.getCurrentTime, + destroy: player.destroy, + }; +} diff --git a/src/YoutubePlayer/index.ts b/src/YoutubePlayer/index.ts index 128f622..22697a0 100644 --- a/src/YoutubePlayer/index.ts +++ b/src/YoutubePlayer/index.ts @@ -1,6 +1,7 @@ import type { YoutubeLink } from "$core/model"; import { renderPlayerContainer } from "@/YoutubePlayer/PlayerContainer.tsx"; import { youtubeEnabled } from "$core/store/youtube.ts"; +import { loadYoutubeAdapter } from "@/YoutubePlayer/adapter.ts"; export interface YoutubePlayer { loadVideo(videoId: string): Promise; @@ -32,15 +33,7 @@ youtubeEnabled.subscribe((value) => { async function createPlayer(): Promise { const container = await renderPlayerContainer(); - const { default: Player } = await import("youtube-player"); - const player = Player(container.htmlElement, { - host: "https://www.youtube-nocookie.com", - playerVars: { - rel: 0, - autoplay: 0, - modestbranding: 1, - }, - }); + const player = await loadYoutubeAdapter(container.htmlElement); function updatePlayerSize() { player?.setSize(window.innerWidth, window.innerHeight); @@ -51,7 +44,7 @@ async function createPlayer(): Promise { const result = { loadVideo(videoId: string) { - return player.loadVideoById({ videoId }); + return player.loadVideoById(videoId); }, async play() { await player.playVideo(); @@ -62,14 +55,7 @@ async function createPlayer(): Promise { container.setVisible(false); }, async waitForStop() { - return new Promise((resolve) => { - player.on("stateChange", (event) => { - if (event.data === 0) { - // Video has ended - resolve(); - } - }); - }); + await player.waitForStop(); }, }; container.addEventListener("stop", () => { diff --git a/src/components/solid/organisms/Reader/Reader.tsx b/src/components/solid/organisms/Reader/Reader.tsx index a1fd771..e2ecc82 100644 --- a/src/components/solid/organisms/Reader/Reader.tsx +++ b/src/components/solid/organisms/Reader/Reader.tsx @@ -14,6 +14,7 @@ import { type DelayControl, DelayIndicator } from "@/components/solid/atoms/Dela import { youtubeEnabled } from "$core/store/youtube.ts"; import { usePersistentStore } from "@/components/solid/hooks/usePersistentStore.ts"; import { YoutubePlayButton } from "@/components/solid/atoms/YoutubePlayButton.tsx"; +import { coerceToArray } from "$core/utils/coerceToArray.ts"; export const Reader: Component<{ dojoInfo: DojoInfo; speechPack: SpeechPack }> = (props) => { const techniqueStore = createTechniqueStore(props.dojoInfo.id); @@ -56,7 +57,7 @@ export const Reader: Component<{ dojoInfo: DojoInfo; speechPack: SpeechPack }> = ready={playerLoaded()} onClickAutoPlay={() => setAutoPlay(!autoPlay())} autoPlayEnabled={autoPlay()} - youtube={youtubeLinks(lastTechnique()?.metadata?.youtube)} + youtube={coerceToArray(lastTechnique()?.metadata?.youtube)} /> ); }; - -function youtubeLinks(links: YoutubeLink[] | YoutubeLink | undefined): YoutubeLink[] { - if (Array.isArray(links)) return links; - return links ? [links] : []; -} diff --git a/src/components/solid/organisms/VideoPackEditor/VideoPackEditor.tsx b/src/components/solid/organisms/VideoPackEditor/VideoPackEditor.tsx new file mode 100644 index 0000000..04a1dc0 --- /dev/null +++ b/src/components/solid/organisms/VideoPackEditor/VideoPackEditor.tsx @@ -0,0 +1,228 @@ +import { type Component, createEffect, createSignal, onCleanup } from "solid-js"; +import type { VideoPack } from "$core/model/VideoPack.ts"; +import type { BaseTechnique, TechniqueTree, YoutubeLink } from "$core/model"; +import { resolveTechniqueTrees } from "$core/resolveExamTables/resolveExamTables.ts"; +import { loadYoutubeAdapter, type YoutubeAdapter } from "@/YoutubePlayer/adapter.ts"; +import { cls } from "$core/utils/cls.ts"; +import { SimpleButton } from "@/components/solid/atoms/SimpleButton.tsx"; +import { buildTechniqueTree } from "$core/buildExamTable/buildExamTable.ts"; + +interface TechniqueVideo extends BaseTechnique { + video: YoutubeLink; +} + +export const VideoPackEditor: Component<{ + videoPack: VideoPack; +}> = (props) => { + const [selectedVideo, setSelectedVideo] = createSignal(null); + const [updatedVideos, setUpdatedVideos] = createSignal>( + JSON.parse(localStorage.getItem(`videoPack-${props.videoPack.name}`) ?? "{}"), + ); + + createEffect(() => { + localStorage.setItem(`videoPack-${props.videoPack.name}`, JSON.stringify(updatedVideos())); + }); + + const resolvedVideos: TechniqueVideo[] = resolveTechniqueTrees([props.videoPack.videos], (technique, videos) => ({ + ...technique, + videos, + })).flatMap(({ videos, ...baseTechnique }) => videos.map((video) => ({ ...baseTechnique, video }))); + + function updateVideo(video: TechniqueVideo) { + setUpdatedVideos((prev) => ({ ...prev, [video.video.id!]: video })); + } + + function selectVideo(video: TechniqueVideo) { + setSelectedVideo(updatedVideos()[video.video.id!] ?? video); + } + + async function copyToClipboard() { + const withUpdatedTechniques = resolvedVideos.map( + (techniqueVideo) => updatedVideos()[techniqueVideo.video.id!] ?? techniqueVideo, + ); + + const tree: TechniqueTree = buildTechniqueTree( + withUpdatedTechniques, + (technique, existingVideos) => (existingVideos ? [...existingVideos, technique.video] : [technique.video]), + ); + + const videoPack: VideoPack = { + ...props.videoPack, + videos: tree, + }; + + await navigator.clipboard.writeText(JSON.stringify(videoPack, null, 2)); + alert("Copied to clipboard"); + } + + return ( +
+

{props.videoPack.name}

+

+ + {props.videoPack.source} + +

+
+
+ setUpdatedVideos({})} /> + + + + + + + + + + + + + + {resolvedVideos.map((techniqueVideo, index) => ( + selectVideo(updatedVideos()[techniqueVideo.video.id!] ?? techniqueVideo)} + > + + + + + + + + ))} + +
ExecutionAttackDefenceDirectionVideodirty
{techniqueVideo.execution}{techniqueVideo.attack}{techniqueVideo.defence}{techniqueVideo.direction}{techniqueVideo.video.title}{updatedVideos()[techniqueVideo.video.id!] != null ? "x" : ""}
+
+
{selectedVideo() && }
+
+
+ ); +}; + +const VideoEditor: Component<{ video: TechniqueVideo; onChange: (video: TechniqueVideo) => void }> = (props) => { + const [start, setStart] = createSignal(0); + const [end, setEnd] = createSignal(props.video.video.durationSeconds); + + let player: YoutubeAdapter | null = null; + let resizeObserver: ResizeObserver | null = null; + + async function setPlayerElement(element: HTMLDivElement) { + const playerElement = document.createElement("div"); + element.append(playerElement); + player = await loadYoutubeAdapter(playerElement); + resizeObserver = new ResizeObserver(() => { + player?.setSize(element.clientWidth, element.clientHeight); + }); + resizeObserver.observe(element); + await player?.loadVideoById(props.video.video.videoId, start(), end()); + await player?.playVideo(); + } + + createEffect(() => { + setStart(props.video.video.startSeconds ?? 0); + setEnd(props.video.video.endSeconds ?? props.video.video.durationSeconds); + }); + + async function updateVideo() { + await player?.loadVideoById(props.video.video.videoId, start(), end()); + await player?.playVideo(); + + props.onChange({ + ...props.video, + video: { + ...props.video.video, + startSeconds: start(), + endSeconds: end(), + }, + }); + } + + onCleanup(() => { + player?.destroy(); + resizeObserver?.disconnect(); + }); + + useKeyboard("KeyS", async () => { + if (player) { + setStart(await player.getCurrentTime()); + } + }); + useKeyboard("KeyE", async () => { + if (player) { + setEnd(await player.getCurrentTime()); + } + }); + + createEffect(() => { + triggerChangeFor(props.video.video); + const video = props.video.video; + player?.loadVideoById(video.videoId, video.startSeconds, video.endSeconds); + }); + + function updateStart(event: Event) { + setStart(Number((event.target as HTMLInputElement).value)); + updateVideo(); + } + function updateEnd(event: Event) { + setEnd(Number((event.target as HTMLInputElement).value)); + updateVideo(); + } + + return ( +
+
+ +
{props.video.video.videoId}
+ +
{props.video.video.title}
+ + + + + + + + +
+
+
+ ); +}; + +function useKeyboard(code: string, callback: () => void) { + const listener = (event: KeyboardEvent) => { + if (event.code === code) { + callback(); + } + }; + window.addEventListener("keydown", listener); + onCleanup(() => window.removeEventListener("keydown", listener)); +} + +/** + * Do nothing. This function is just called to make use of a value so that solid-js reactivity is triggered + * @param ignoredValue + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function triggerChangeFor(value: unknown) {} diff --git a/src/components/solid/organisms/VideoPackEditor/index.ts b/src/components/solid/organisms/VideoPackEditor/index.ts new file mode 100644 index 0000000..9e941e6 --- /dev/null +++ b/src/components/solid/organisms/VideoPackEditor/index.ts @@ -0,0 +1 @@ +export { VideoPackEditor } from "./VideoPackEditor"; diff --git a/src/core/buildExamTable/buildExamTable.ts b/src/core/buildExamTable/buildExamTable.ts index 01fba30..1c12423 100644 --- a/src/core/buildExamTable/buildExamTable.ts +++ b/src/core/buildExamTable/buildExamTable.ts @@ -1,18 +1,30 @@ -import type { Attacks, Defences, Directions, Table, Technique } from "$core/model"; +import type { BaseTechnique, Table, Technique, TechniqueTree } from "$core/model"; +import type { Attacks, Defences, Directions } from "$core/model/TechniqueTree.ts"; export function buildExamTable(list: Iterable): Table { - const examTable: Table = {}; + return buildTechniqueTree(list, (technique) => technique.metadata); +} + +export function buildTechniqueTree( + list: Iterable, + mapFn: (technique: T, existingTechnique?: M) => M, +): TechniqueTree { + const tree: TechniqueTree = {}; for (const technique of list) { - addTechniqueToTable(examTable, technique); + addTechniqueToTable(tree, technique, mapFn); } - return examTable; + return tree; } -function addTechniqueToTable(examTable: Table, technique: Technique): void { - const execution: Attacks = getOrCreate(examTable, technique.execution, createObject); - const attack: Defences = getOrCreate(execution, technique.attack, createObject); - const defence: Directions = getOrCreate(attack, technique.defence, createObject); - defence[technique.direction] = technique.metadata; +function addTechniqueToTable( + examTable: TechniqueTree, + technique: T, + mapFn: (technique: T, existingTechnique?: M) => M, +): void { + const execution: Attacks = getOrCreate(examTable, technique.execution, createObject); + const attack: Defences = getOrCreate(execution, technique.attack, createObject); + const defence: Directions = getOrCreate(attack, technique.defence, createObject); + defence[technique.direction] = mapFn(technique, defence[technique.direction]); } function getOrCreate(object: Partial>, key: K, createNew: () => V): V { diff --git a/src/core/model/Exam.ts b/src/core/model/Exam.ts index 54c9d6d..b53658b 100644 --- a/src/core/model/Exam.ts +++ b/src/core/model/Exam.ts @@ -1,14 +1,8 @@ -import type { Direction } from "./Direction"; import type { TechniqueMetadata } from "./TechniqueMetadata"; -import type { Defence } from "./Defence"; -import type { Attack } from "./Attack"; -import type { Execution } from "./Execution"; import type { TranslatedText } from "$core/model/Dojo.ts"; +import type { TechniqueTree } from "$core/model/TechniqueTree.ts"; -export type Directions = Partial>; -export type Defences = Partial>; -export type Attacks = Partial>; -export type Table = Partial>; +export type Table = TechniqueTree; export type WellKnownExam = "kyu5" | "kyu4" | "kyu3" | "kyu2" | "kyu1" | "dan1" | "dan2" | "dan3" | "dan4"; export type ExamLabel = | { diff --git a/src/core/model/TechniqueMetadata.ts b/src/core/model/TechniqueMetadata.ts index c1cbca6..aa60a93 100644 --- a/src/core/model/TechniqueMetadata.ts +++ b/src/core/model/TechniqueMetadata.ts @@ -2,7 +2,10 @@ export interface TechniqueMetadata { youtube?: YoutubeLink | YoutubeLink[]; } export interface YoutubeLink { + id?: string; videoId: string; + startSeconds?: number; + endSeconds?: number; durationSeconds: number; title: string; } diff --git a/src/core/model/TechniqueTree.ts b/src/core/model/TechniqueTree.ts new file mode 100644 index 0000000..e8a7ac3 --- /dev/null +++ b/src/core/model/TechniqueTree.ts @@ -0,0 +1,9 @@ +import type { Direction } from "$core/model/Direction.ts"; +import type { Defence } from "$core/model/Defence.ts"; +import type { Attack } from "$core/model/Attack.ts"; +import type { Execution } from "$core/model/Execution.ts"; + +export type Directions = Partial>; +export type Defences = Partial>>; +export type Attacks = Partial>>; +export type TechniqueTree = Partial>>; diff --git a/src/core/model/VideoPack.ts b/src/core/model/VideoPack.ts new file mode 100644 index 0000000..d5711e4 --- /dev/null +++ b/src/core/model/VideoPack.ts @@ -0,0 +1,8 @@ +import type { TechniqueTree } from "$core/model/TechniqueTree.ts"; +import type { YoutubeLink } from "$core/model/TechniqueMetadata.ts"; + +export interface VideoPack { + name: string; + source: string; + videos: TechniqueTree; +} diff --git a/src/core/model/index.ts b/src/core/model/index.ts index 1fe408e..b45d1bd 100644 --- a/src/core/model/index.ts +++ b/src/core/model/index.ts @@ -6,3 +6,4 @@ export * from "./TechniqueMetadata"; export * from "./Technique"; export * from "./Exam"; export * from "./SpeechPack"; +export * from "./TechniqueTree"; diff --git a/src/core/resolveExamTables/resolveExamTables.ts b/src/core/resolveExamTables/resolveExamTables.ts index 46107e6..f43c0a0 100644 --- a/src/core/resolveExamTables/resolveExamTables.ts +++ b/src/core/resolveExamTables/resolveExamTables.ts @@ -1,13 +1,23 @@ -import type { Technique, Exam } from "$core/model"; +import type { Technique, Exam, TechniqueTree, BaseTechnique } from "$core/model"; export function resolveExamTables(examTables: Exam[]): Technique[] { - const result: Technique[] = []; - for (const exam of examTables) { - for (const [execution, attacks] of entries(exam.techniques)) { + return resolveTechniqueTrees( + examTables.map((exam) => exam.techniques), + (technique, metadata) => ({ ...technique, metadata }), + ); +} + +export function resolveTechniqueTrees( + trees: TechniqueTree[], + mapFn: (technique: BaseTechnique, metadata: M) => T, +): T[] { + const result: T[] = []; + for (const tree of trees) { + for (const [execution, attacks] of entries(tree)) { for (const [attack, defences] of entries(attacks)) { for (const [defence, directions] of entries(defences)) { for (const [direction, metadata] of entries(directions)) { - result.push({ execution, attack, defence, direction, metadata }); + result.push(mapFn({ execution, attack, defence, direction }, metadata)); } } } diff --git a/src/core/utils/coerceToArray.test.ts b/src/core/utils/coerceToArray.test.ts new file mode 100644 index 0000000..1261bd1 --- /dev/null +++ b/src/core/utils/coerceToArray.test.ts @@ -0,0 +1,16 @@ +import { coerceToArray } from "$core/utils/coerceToArray.ts"; + +describe("coerceToArray", () => { + it("returns empty array for null", () => { + expect(coerceToArray(null)).toEqual([]); + expect(coerceToArray(undefined)).toEqual([]); + }); + + it("returns single element array for single element", () => { + expect(coerceToArray(2)).toEqual([2]); + }); + it("returns the array for an array", () => { + expect(coerceToArray([2])).toEqual([2]); + expect(coerceToArray([2, 3])).toEqual([2, 3]); + }); +}); diff --git a/src/core/utils/coerceToArray.ts b/src/core/utils/coerceToArray.ts new file mode 100644 index 0000000..357f3a2 --- /dev/null +++ b/src/core/utils/coerceToArray.ts @@ -0,0 +1,9 @@ +export function coerceToArray(input: T | T[] | undefined | null): T[] { + if (input == null) { + return []; + } + if (Array.isArray(input)) { + return input; + } + return [input]; +} diff --git a/src/data/videopacks/aikido-kompendium/index.ts b/src/data/videopacks/aikido-kompendium/index.ts new file mode 100644 index 0000000..1094d7a --- /dev/null +++ b/src/data/videopacks/aikido-kompendium/index.ts @@ -0,0 +1,2572 @@ +import type { VideoPack } from "$core/model/VideoPack"; + +export default { + name: "Aikido Kompendium", + source: "https://www.aikido-kompendium.de", + videos: { + "suwari waza": { + "ryote dori": { + "kokyu ho": { + "single-direction": [ + { + title: "suwari waza ryote dori kokyu ho", + videoId: "3yd1kGHIviA", + durationSeconds: 20, + id: "0", + startSeconds: 0, + endSeconds: 20, + }, + ], + }, + }, + "ai hanmi katate dori": { + ikkyo: { + omote: [ + { + title: "suwari waza ai hanmi katate dori ikkyo omote", + videoId: "HZOkvZDAh34", + durationSeconds: 27, + id: "13", + startSeconds: 0, + endSeconds: 27, + }, + ], + ura: [ + { + title: "suwari waza ai hanmi katate dori ikkyo ura", + videoId: "QUNg_jthIqM", + durationSeconds: 21, + id: "14", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "suwari waza ai hanmi katate dori irimi nage", + videoId: "dHhOLufvx3U", + durationSeconds: 24, + id: "15", + }, + ], + }, + }, + "shomen uchi": { + ikkyo: { + omote: [ + { + title: "suwari waza shomen uchi ikkyo omote", + videoId: "ODF1mKYFT50", + durationSeconds: 21, + id: "16", + }, + ], + ura: [ + { + title: "suwari waza shomen uchi ikkyo ura", + videoId: "dm7lj0RaEzw", + durationSeconds: 19, + id: "17", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "suwari waza shomen uchi irimi nage", + videoId: "ZrvHAhQXyyE", + durationSeconds: 22, + id: "18", + }, + ], + }, + nikyo: { + omote: [ + { + title: "suwari waza shomen uchi nikyo omote", + videoId: "meNnP_iyjSY", + durationSeconds: 30, + id: "59", + }, + ], + ura: [ + { + title: "suwari waza shomen uchi nikyo ura", + videoId: "2gWlA-rMtic", + durationSeconds: 31, + id: "60", + }, + ], + }, + sankyo: { + omote: [ + { + title: "suwari waza shomen uchi sankyo omote", + videoId: "OaKRmnjJdK8", + durationSeconds: 29, + id: "61", + }, + ], + ura: [ + { + title: "suwari waza shomen uchi sankyo ura", + videoId: "f3gHuS8Bp3A", + durationSeconds: 29, + id: "62", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "suwari waza shomen uchi yonkyo omote", + videoId: "X-fctXW0Lmg", + durationSeconds: 21, + id: "63", + }, + ], + ura: [ + { + title: "suwari waza shomen uchi yonkyo ura", + videoId: "a-jasPR2So0", + durationSeconds: 26, + id: "64", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "suwari waza shomen uchi kote gaeshi", + videoId: "9HNkdOJmT3k", + durationSeconds: 28, + id: "65", + }, + ], + }, + gokyo: { + "single-direction": [ + { + title: "suwari waza shomen uchi gokyo", + videoId: "t9aPIPxxdzs", + durationSeconds: 22, + id: "227", + }, + ], + }, + }, + "gyuako hanmi katate dori": { + ikkyo: { + omote: [ + { + title: "suwari waza katate dori ikkyo omote", + videoId: "i3bgZsdTmyQ", + durationSeconds: 24, + id: "51", + }, + ], + ura: [ + { + title: "suwari waza katate dori ikkyo ura", + videoId: "dQF5oxxDD30", + durationSeconds: 24, + id: "52", + }, + ], + }, + nikyo: { + omote: [ + { + title: "suwari waza katate dori nikyo omote", + videoId: "ypXXzb2YHFY", + durationSeconds: 29, + id: "53", + }, + ], + ura: [ + { + title: "suwari waza katate dori nikyo ura", + videoId: "6JBCfxORKJQ", + durationSeconds: 29, + id: "54", + }, + ], + }, + sankyo: { + omote: [ + { + title: "suwari waza katate dori sankyo omote", + videoId: "B5fVD3L47zg", + durationSeconds: 25, + id: "150", + }, + ], + ura: [ + { + title: "suwari waza katate dori sankyo ura", + videoId: "sunQhz4qybw", + durationSeconds: 24, + id: "151", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "suwari waza katate dori yonkyo omote", + videoId: "Y8Qym8RlPOw", + durationSeconds: 20, + id: "152", + }, + ], + ura: [ + { + title: "suwari waza katate dori yonkyo ura", + videoId: "cf9NaXqF_Ck", + durationSeconds: 24, + id: "153", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "suwari waza katate dori irimi nage", + videoId: "MQgDSlsfNlQ", + durationSeconds: 35, + id: "154", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "suwari waza katate dori kote gaeshi", + videoId: "9siBUjYwsp4", + durationSeconds: 37, + id: "155", + }, + ], + }, + }, + "kata dori": { + ikkyo: { + omote: [ + { + title: "suwari waza kata dori ikkyo omote", + videoId: "N7L2BI0PYeE", + durationSeconds: 24, + id: "55", + }, + ], + ura: [ + { + title: "suwari waza kata dori ikkyo ura", + videoId: "_Ap_QOoTlzY", + durationSeconds: 23, + id: "56", + }, + ], + }, + nikyo: { + omote: [ + { + title: "suwari waza kata dori nikyo omote", + videoId: "dMAiRF88oDc", + durationSeconds: 24, + id: "57", + }, + ], + ura: [ + { + title: "suwari waza kata dori nikyo ura", + videoId: "ZJPiqlLLVv4", + durationSeconds: 25, + id: "58", + }, + ], + }, + sankyo: { + omote: [ + { + title: "suwari waza kata dori sankyo omote", + videoId: "TgWrIrGSLYY", + durationSeconds: 24, + id: "146", + }, + ], + ura: [ + { + title: "suwari waza kata dori sankyo ura", + videoId: "NbjiWpogzv0", + durationSeconds: 29, + id: "147", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "suwari waza kata dori yonkyo omote", + videoId: "OszzoCfX0ng", + durationSeconds: 22, + id: "148", + }, + ], + ura: [ + { + title: "suwari waza kata dori yonkyo ura", + videoId: "ivozMrI_jYo", + durationSeconds: 24, + id: "149", + }, + ], + }, + }, + "yokomen uchi": { + ikkyo: { + omote: [ + { + title: "suwari waza yokomen uchi ikkyo omote", + videoId: "5YhNgMI8Do4", + durationSeconds: 20, + id: "135", + }, + ], + ura: [ + { + title: "suwari waza yokomen uchi ikkyo ura", + videoId: "ObooPb_TJ7A", + durationSeconds: 15, + id: "136", + }, + ], + }, + nikyo: { + omote: [ + { + title: "suwari waza yokomen uchi nikyo omote", + videoId: "mxc-WO3P_m0", + durationSeconds: 35, + id: "137", + }, + ], + ura: [ + { + title: "suwari waza yokomen uchi nikyo ura", + videoId: "oycJt1PITbU", + durationSeconds: 40, + id: "138", + }, + ], + }, + sankyo: { + omote: [ + { + title: "suwari waza yokomen uchi sankyo omote", + videoId: "YymMQHtHFkY", + durationSeconds: 25, + id: "139", + }, + ], + ura: [ + { + title: "suwari waza yokomen uchi sankyo ura", + videoId: "x9b8XoN-bso", + durationSeconds: 26, + id: "140", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "suwari waza yokomen uchi yonkyo omote", + videoId: "DrTysa9KovM", + durationSeconds: 19, + id: "141", + }, + ], + ura: [ + { + title: "suwari waza yokomen uchi yonkyo ura", + videoId: "jiE7oNKTsaw", + durationSeconds: 21, + id: "142", + }, + ], + }, + gokyo: { + "single-direction": [ + { + title: "suwari waza yokomen uchi gokyo", + videoId: "0csSSYQdfyY", + durationSeconds: 17, + id: "143", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "suwari waza yokomen uchi irimi nage 1", + videoId: "vHmvCinsSkE", + durationSeconds: 18, + id: "144", + }, + { + title: "suwari waza yokomen uchi irimi nage 2", + videoId: "PweCVZFwDLE", + durationSeconds: 8, + id: "145", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "suwari waza yokomen uchi kote gaeshi", + videoId: "dfGpSzw4vYA", + durationSeconds: 27, + id: "226", + }, + ], + }, + }, + }, + "tachi waza": { + "ai hanmi katate dori": { + ikkyo: { + omote: [ + { + title: "tachi waza ai hanmi katate dori ikkyo omote", + videoId: "WG3FmJtdIf4", + durationSeconds: 23, + id: "1", + }, + ], + ura: [ + { + title: "tachi waza ai hanmi katate dori ikkyo ura", + videoId: "CZ3vw6kbArk", + durationSeconds: 24, + id: "2", + }, + ], + }, + "shiho nage": { + omote: [ + { + title: "tachi waza ai hanmi katate dori shiho nage omote", + videoId: "mjRlD996OgU", + durationSeconds: 22, + id: "3", + }, + ], + ura: [ + { + title: "tachi waza ai hanmi katate dori shiho nage ura", + videoId: "C5oqSt5syB8", + durationSeconds: 19, + id: "4", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "tachi waza ai hanmi katate dori irimi nage", + videoId: "gxDKLqY0n2s", + durationSeconds: 23, + id: "5", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza ai hanmi katate dori nikyo omote", + videoId: "DZnD628Vgqw", + durationSeconds: 29, + id: "34", + }, + ], + ura: [ + { + title: "tachi waza ai hanmi katate dori nikyo ura", + videoId: "lrxsIu4YL0c", + durationSeconds: 28, + id: "35", + }, + ], + }, + "uchi kaiten sankyo": { + "single-direction": [ + { + title: "tachi waza ai hanmi katate dori sankyo uchi kaiten", + videoId: "9HGq0mh1sWY", + durationSeconds: 28, + id: "36", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza ai hanmi katate dori kote gaeshi", + videoId: "yfPIir-K8m8", + durationSeconds: 32, + id: "37", + }, + ], + }, + "ude kime nage": { + omote: [ + { + title: "tachi waza ai hanmi katate dori ude kime nage omote", + videoId: "gNL_f141PHE", + durationSeconds: 16, + id: "38", + }, + ], + ura: [ + { + title: "tachi waza ai hanmi katate dori ude kime nage ura", + videoId: "dDYgS6YAYY8", + durationSeconds: 14, + id: "39", + }, + ], + }, + "kokyu nage": { + "single-direction": [ + { + title: "tachi waza ai hanmi katate dori kokyu nage 01", + videoId: "z9UzvBgVjQ4", + durationSeconds: 15, + id: "124", + }, + { + title: "tachi waza ai hanmi katate dori kokyu nage 02", + videoId: "U5q2eLL_Lqk", + durationSeconds: 15, + id: "125", + }, + { + title: "tachi waza ai hanmi katate dori kokyu nage 03", + videoId: "1MTGKukD-EY", + durationSeconds: 16, + id: "126", + }, + { + title: "tachi waza ai hanmi katate dori kokyu nage 04", + videoId: "QLnwGmHjZs8", + durationSeconds: 16, + id: "127", + }, + { + title: "tachi waza ai hanmi katate dori kokyu nage 05", + videoId: "kDqr1JOLD6M", + durationSeconds: 19, + id: "128", + }, + ], + }, + "koshi nage": { + "single-direction": [ + { + title: "tachi waza ai hanmi katate dori koshi nage 01", + videoId: "YSNs3Ek1540", + durationSeconds: 23, + id: "209", + }, + { + title: "tachi waza ai hanmi katate dori koshi nage 02", + videoId: "wExBIpHy8xs", + durationSeconds: 25, + id: "210", + }, + ], + }, + }, + "gyuako hanmi katate dori": { + "shiho nage": { + omote: [ + { + title: "tachi waza katate dori shiho nage omote", + videoId: "-e3J0i0ejCc", + durationSeconds: 18, + id: "6", + }, + ], + ura: [ + { + title: "tachi waza katate dori shiho nage ura", + videoId: "0FHanKF2FYE", + durationSeconds: 19, + id: "7", + }, + ], + }, + "tenchi nage": { + omote: [ + { + title: "tachi waza katate dori tenchi nage omote", + videoId: "bh-60q0X4K8", + durationSeconds: 18, + id: "8", + }, + ], + ura: [ + { + title: "tachi waza katate dori tenchi nage ura", + videoId: "YwJ0_gq0P7U", + durationSeconds: 19, + id: "9", + }, + ], + }, + ikkyo: { + omote: [ + { + title: "tachi waza katate dori ikkyo omote", + videoId: "_4LIwYIWIn4", + durationSeconds: 27, + id: "19", + }, + ], + ura: [ + { + title: "tachi waza katate dori ikkyo ura", + videoId: "Hq3ni6cmXpo", + durationSeconds: 21, + id: "20", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza katate dori nikyo omote", + videoId: "tKR7WAtTDCI", + durationSeconds: 32, + id: "21", + }, + ], + ura: [ + { + title: "tachi waza katate dori nikyo ura", + videoId: "VXS6vrVAud4", + durationSeconds: 29, + id: "22", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza katate dori kote gaeshi", + videoId: "5D8ovPKcGD0", + durationSeconds: 33, + id: "23", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "tachi waza katate dori irimi nage", + videoId: "gPTWMlzcHa0", + durationSeconds: 25, + id: "24", + }, + ], + }, + "uchi kaiten nage": { + omote: [ + { + title: "tachi waza katate dori uchi kaiten nage omote", + videoId: "brheKOH6tBc", + durationSeconds: 22, + id: "25", + }, + ], + ura: [ + { + title: "tachi waza katate dori uchi kaiten nage ura", + videoId: "gACnFLUzVuY", + durationSeconds: 21, + id: "26", + }, + ], + }, + "ude kime nage": { + omote: [ + { + title: "tachi waza katate dori ude kime nage omote", + videoId: "pYEz4Fey_4g", + durationSeconds: 17, + id: "27", + }, + ], + ura: [ + { + title: "tachi waza katate dori ude kime nage ura", + videoId: "uinQr-gE_vk", + durationSeconds: 14, + id: "28", + }, + ], + }, + "sokumen irimi nage": { + "single-direction": [ + { + title: "tachi waza katate dori sokumen irimi nage", + videoId: "0duNhOK2-bU", + durationSeconds: 19, + id: "29", + }, + ], + }, + "kokyu nage": { + "single-direction": [ + { + title: "tachi waza katate dori kokyu nage 01", + videoId: "sIMCbchcTbA", + durationSeconds: 16, + id: "129", + }, + { + title: "tachi waza katate dori kokyu nage 02", + videoId: "N3WOhF74QGI", + durationSeconds: 14, + id: "130", + }, + { + title: "tachi waza katate dori kokyu nage 03", + videoId: "hJP_KOU8zMI", + durationSeconds: 16, + id: "131", + }, + { + title: "tachi waza katate dori kokyu nage 04", + videoId: "OreDX3cRAIQ", + durationSeconds: 17, + id: "132", + }, + { + title: "tachi waza katate dori kokyu nage 05", + videoId: "RgI-Unqavgs", + durationSeconds: 14, + id: "133", + }, + { + title: "tachi waza katate dori kokyu nage 06", + videoId: "6WYY66TtI3Q", + durationSeconds: 14, + id: "134", + }, + ], + }, + sankyo: { + omote: [ + { + title: "tachi waza katate dori sankyo omote", + videoId: "bUtn-Otj7Vs", + durationSeconds: 41, + id: "185", + }, + ], + ura: [ + { + title: "tachi waza katate dori sankyo ura", + videoId: "hmOEEhtM3Oo", + durationSeconds: 38, + id: "186", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "tachi waza katate dori yonkyo omote", + videoId: "tgnTZSF5Zds", + durationSeconds: 31, + id: "187", + }, + ], + ura: [ + { + title: "tachi waza katate dori yonkyo ura", + videoId: "xxh-gs302k4", + durationSeconds: 31, + id: "188", + }, + ], + }, + "sumi otoshi": { + "single-direction": [ + { + title: "tachi waza katate dori sumi otoshi", + videoId: "afl0lZj4brE", + durationSeconds: 17, + id: "189", + }, + ], + }, + "aiki otoshi": { + "single-direction": [ + { + title: "tachi waza katate dori aiki otoshi", + videoId: "XOAgW5nzAuY", + durationSeconds: 26, + id: "190", + }, + ], + }, + "koshi nage": { + "single-direction": [ + { + title: "tachi waza katate dori koshi nage 01", + videoId: "4qObC2e94_4", + durationSeconds: 24, + id: "191", + }, + { + title: "tachi waza katate dori koshi nage 02", + videoId: "R4pYyYJE47c", + durationSeconds: 21, + id: "192", + }, + { + title: "tachi waza katate dori koshi nage 03", + videoId: "l49Z6VxUhSY", + durationSeconds: 24, + id: "193", + }, + ], + }, + }, + "shomen uchi": { + ikkyo: { + omote: [ + { + title: "tachi waza shomen uchi ikkyo omote", + videoId: "uPmyiNCoB9I", + durationSeconds: 24, + id: "10", + }, + ], + ura: [ + { + title: "tachi waza shomen uchi ikkyo ura", + videoId: "3xzsCVm5NIE", + durationSeconds: 21, + id: "11", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "tachi waza shomen uchi irimi nage", + videoId: "SaMqbWh--Aw", + durationSeconds: 20, + id: "12", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza shomen uchi kote gaeshi", + videoId: "we19G722ClQ", + durationSeconds: 30, + id: "40", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza shomen uchi nikyo omote", + videoId: "DLeKNmWAeaQ", + durationSeconds: 29, + id: "118", + }, + ], + ura: [ + { + title: "tachi waza shomen uchi nikyo ura", + videoId: "-jpi60tjS2g", + durationSeconds: 32, + id: "119", + }, + ], + }, + sankyo: { + omote: [ + { + title: "tachi waza shomen uchi sankyo omote", + videoId: "iUPTb2ZbxyA", + durationSeconds: 41, + id: "120", + }, + ], + ura: [ + { + title: "tachi waza shomen uchi sankyo ura", + videoId: "x1x_tIfkbkM", + durationSeconds: 31, + id: "121", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "tachi waza shomen uchi yonkyo omote", + videoId: "J_Rb7w6K4cY", + durationSeconds: 25, + id: "122", + }, + ], + ura: [ + { + title: "tachi waza shomen uchi yonkyo ura", + videoId: "2hvZBBGvFeY", + durationSeconds: 27, + id: "123", + }, + ], + }, + "shiho nage": { + "single-direction": [ + { + title: "tachi waza shomen uchi shiho nage omote", + videoId: "bbTBdPUBX4A", + durationSeconds: 22, + id: "194", + }, + ], + }, + "soto kaiten nage": { + omote: [ + { + title: "tachi waza shomen uchi soto kaiten nage omote", + videoId: "ja8BX5WYgwI", + durationSeconds: 19, + id: "195", + }, + ], + ura: [ + { + title: "tachi waza shomen uchi soto kaiten nage ura", + videoId: "s0UNsz4ywRk", + durationSeconds: 19, + id: "196", + }, + ], + }, + "uchi kaiten nage": { + omote: [ + { + title: "tachi waza shomen uchi uchi kaiten nage omote", + videoId: "vpAEiet-Qjg", + durationSeconds: 24, + id: "197", + }, + ], + ura: [ + { + title: "tachi waza shomen uchi uchi kaiten nage ura", + videoId: "ZBIGm-j-Q_k", + durationSeconds: 26, + id: "198", + }, + ], + }, + "kokyu nage": { + "single-direction": [ + { + title: "tachi waza shomen uchi kokyu nage 01", + videoId: "7hn7YTuOUM0", + durationSeconds: 19, + id: "199", + }, + { + title: "tachi waza shomen uchi kokyu nage 02", + videoId: "0bKV4RbaFjc", + durationSeconds: 18, + id: "200", + }, + { + title: "tachi waza shomen uchi kokyu nage 03", + videoId: "2N7UOXPpKgM", + durationSeconds: 18, + id: "201", + }, + { + title: "tachi waza shomen uchi kokyu nage 04", + videoId: "pYBqXsj5tL4", + durationSeconds: 18, + id: "202", + }, + { + title: "tachi waza shomen uchi kokyu nage 05", + videoId: "h6oxRXTWvH0", + durationSeconds: 21, + id: "203", + }, + { + title: "tachi waza shomen uchi kokyu nage 06", + videoId: "nMcw3uYa49A", + durationSeconds: 18, + id: "204", + }, + ], + }, + gokyo: { + "single-direction": [ + { + title: "tachi waza shomen uchi gokyo", + videoId: "91pLpZySRl4", + durationSeconds: 14, + id: "273", + }, + ], + }, + }, + "kata dori": { + ikkyo: { + omote: [ + { + title: "tachi waza kata dori ikkyo omote", + videoId: "SXl27xFPwpA", + durationSeconds: 26, + id: "30", + }, + ], + ura: [ + { + title: "tachi waza kata dori ikkyo ura", + videoId: "ucbkn5E6oyA", + durationSeconds: 22, + id: "31", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza kata dori nikyo omote", + videoId: "88MLWhKQAds", + durationSeconds: 28, + id: "32", + }, + ], + ura: [ + { + title: "tachi waza kata dori nikyo ura", + videoId: "e4nL1r4SQs8", + durationSeconds: 29, + id: "33", + }, + ], + }, + sankyo: { + omote: [ + { + title: "tachi waza kata dori sankyo omote", + videoId: "wuz40efmmh8", + durationSeconds: 46, + id: "181", + }, + ], + ura: [ + { + title: "tachi waza kata dori sankyo ura", + videoId: "HXKdWpYm318", + durationSeconds: 36, + id: "182", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "tachi waza kata dori yonkyo omote", + videoId: "11P8-M_5g-0", + durationSeconds: 31, + id: "183", + }, + ], + ura: [ + { + title: "tachi waza kata dori yonkyo ura", + videoId: "YFgWNGdem8E", + durationSeconds: 28, + id: "184", + }, + ], + }, + }, + "yokomen uchi": { + "shiho nage": { + omote: [ + { + title: "tachi waza yokomen uchi shiho nage omote", + videoId: "tYUTwBxeFU4", + durationSeconds: 20, + id: "41", + }, + ], + ura: [ + { + title: "tachi waza yokomen uchi shiho nage ura", + videoId: "VXR2QZv6GQM", + durationSeconds: 20, + id: "42", + }, + ], + }, + "ude kime nage": { + omote: [ + { + title: "tachi waza yokomen uchi ude kime nage omote", + videoId: "kS036P7tyWU", + durationSeconds: 18, + id: "43", + }, + ], + ura: [ + { + title: "tachi waza yokomen uchi ude kime nage ura", + videoId: "WrQoMKhlgfg", + durationSeconds: 18, + id: "44", + }, + ], + }, + ikkyo: { + omote: [ + { + title: "tachi waza yokomen uchi ikkyo omote", + videoId: "aBU9kwaRDAI", + durationSeconds: 26, + id: "105", + }, + ], + ura: [ + { + title: "tachi waza yokomen uchi ikkyo ura", + videoId: "OxpdecDIx4g", + durationSeconds: 25, + id: "106", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza yokomen uchi nikyo omote", + videoId: "qdqVDdX_gjU", + durationSeconds: 31, + id: "107", + }, + ], + ura: [ + { + title: "tachi waza yokomen uchi nikyo ura", + videoId: "6bTq9TDZ27s", + durationSeconds: 33, + id: "108", + }, + ], + }, + sankyo: { + omote: [ + { + title: "tachi waza yokomen uchi sankyo omote", + videoId: "8QYckaVxS0I", + durationSeconds: 42, + id: "109", + }, + ], + ura: [ + { + title: "tachi waza yokomen uchi sankyo ura", + videoId: "nwhKvBON_ew", + durationSeconds: 33, + id: "110", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "tachi waza yokomen uchi yonkyo omote", + videoId: "lDpYIMCo45A", + durationSeconds: 28, + id: "111", + }, + ], + ura: [ + { + title: "tachi waza yokomen uchi yonkyo ura", + videoId: "T9E6KuKGzAQ", + durationSeconds: 26, + id: "112", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza yokomen uchi kote gaeshi", + videoId: "UUBkZgTeS1A", + durationSeconds: 28, + id: "113", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "tachi waza yokomen uchi irimi nage 01", + videoId: "WUeoBoEA2WY", + durationSeconds: 25, + id: "114", + }, + { + title: "tachi waza yokomen uchi irimi nage 02", + videoId: "H4aCVOo4SAM", + durationSeconds: 21, + id: "115", + }, + { + title: "tachi waza yokomen uchi irimi nage 03", + videoId: "yQdAh-BD4c0", + durationSeconds: 16, + id: "116", + }, + { + title: "tachi waza yokomen uchi irimi nage 04", + videoId: "iNY1cbwUIrE", + durationSeconds: 15, + id: "117", + }, + ], + }, + gokyo: { + "single-direction": [ + { + title: "tachi waza yokomen uchi gokyo ura", + videoId: "gvRvkXn-XGg", + durationSeconds: 31, + id: "205", + }, + ], + }, + "kokyu nage": { + "single-direction": [ + { + title: "tachi waza yokomen uchi kokyu nage", + videoId: "fPxtHgfLVnI", + durationSeconds: 17, + id: "206", + }, + ], + }, + }, + "ryote dori": { + "tenchi nage": { + omote: [ + { + title: "tachi waza ryote dori tenchi nage omote", + videoId: "hN5yNCyimMU", + durationSeconds: 19, + id: "45", + }, + ], + ura: [ + { + title: "tachi waza ryote dori tenchi nage ura", + videoId: "RCn5JPEj0AA", + durationSeconds: 15, + id: "46", + }, + ], + }, + ikkyo: { + omote: [ + { + title: "tachi waza ryote dori ikkyo omote", + videoId: "afQn85XeaU8", + durationSeconds: 26, + id: "81", + }, + ], + ura: [ + { + title: "tachi waza ryote dori ikkyo ura", + videoId: "HudB-p-Zl5w", + durationSeconds: 24, + id: "82", + }, + ], + }, + "shiho nage": { + omote: [ + { + title: "tachi waza ryote dori shiho nage omote", + videoId: "W-XW1_nl66k", + durationSeconds: 21, + id: "83", + }, + ], + ura: [ + { + title: "tachi waza ryote dori shiho nage ura", + videoId: "gZMo-JCALAk", + durationSeconds: 17, + id: "84", + }, + ], + }, + "ude kime nage": { + omote: [ + { + title: "tachi waza ryote dori ude kime nage omote", + videoId: "BK2ADY1jSvM", + durationSeconds: 14, + id: "85", + }, + ], + ura: [ + { + title: "tachi waza ryote dori ude kime nage ura", + videoId: "MGyRZS8OdSE", + durationSeconds: 18, + id: "86", + }, + ], + }, + "kokyu nage": { + "single-direction": [ + { + title: "tachi waza ryote dori kokyu nage 01", + videoId: "D03QJ5FVMTg", + durationSeconds: 15, + id: "87", + }, + { + title: "tachi waza ryote dori kokyu nage 02", + videoId: "W0cJo4TE9EA", + durationSeconds: 14, + id: "88", + }, + { + title: "tachi waza ryote dori kokyu nage 03", + videoId: "h68D-XdD4mY", + durationSeconds: 20, + id: "89", + }, + { + title: "tachi waza ryote dori kokyu nage 04", + videoId: "gEwXOcJvzJQ", + durationSeconds: 14, + id: "90", + }, + { + title: "tachi waza ryote dori kokyu nage 05", + videoId: "a1GeXI6TPjQ", + durationSeconds: 14, + id: "91", + }, + { + title: "tachi waza ryote dori kokyu nage 06", + videoId: "Q3-LEq1xLBM", + durationSeconds: 15, + id: "92", + }, + { + title: "tachi waza ryote dori kokyu nage 07", + videoId: "iw1-CHke6E4", + durationSeconds: 14, + id: "93", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza ryote dori kote gaeshi", + videoId: "DiMvb9qWJtM", + durationSeconds: 28, + id: "207", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "tachi waza ryote dori irimi nage", + videoId: "Y6-3-6g7NNA", + durationSeconds: 27, + id: "208", + }, + ], + }, + }, + "ushiro ryote dori": { + sankyo: { + omote: [ + { + title: "tachi waza ushiro ryote dori sankyo omote 01", + videoId: "yk5TOrSCDk0", + durationSeconds: 31, + id: "47", + }, + { + title: "tachi waza ushiro ryote dori sankyo omote 02", + videoId: "XiBakuXEMf0", + durationSeconds: 30, + id: "48", + }, + ], + ura: [ + { + title: "tachi waza ushiro ryote dori sankyo ura 01", + videoId: "JU--v07PGmA", + durationSeconds: 35, + id: "49", + }, + { + title: "tachi waza ushiro ryote dori sankyo ura 02", + videoId: "PngqAHfdBv4", + durationSeconds: 31, + id: "50", + }, + ], + }, + ikkyo: { + omote: [ + { + title: "tachi waza ushiro ryote dori ikkyo omote", + videoId: "esFrZFBRVfU", + durationSeconds: 28, + id: "94", + }, + ], + ura: [ + { + title: "tachi waza ushiro ryote dori ikkyo ura", + videoId: "6fXx4JG36Rw", + durationSeconds: 28, + id: "95", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza ushiro ryote dori nikyo omote", + videoId: "A-GJGzF9y5c", + durationSeconds: 31, + id: "96", + }, + ], + ura: [ + { + title: "tachi waza ushiro ryote dori nikyo ura", + videoId: "Ga-7V7aRMuU", + durationSeconds: 31, + id: "97", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "tachi waza ushiro ryote dori yonkyo omote", + videoId: "XS7OXIAO1IQ", + durationSeconds: 25, + id: "98", + }, + ], + ura: [ + { + title: "tachi waza ushiro ryote dori yonkyo ura", + videoId: "VoVH7kCM-iA", + durationSeconds: 27, + id: "99", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza ushiro ryote dori kote gaeshi", + videoId: "Cmc8YjYSwAY", + durationSeconds: 32, + id: "100", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "tachi waza ushiro ryote dori irimi nage", + videoId: "lTtNUcbdcI4", + durationSeconds: 22, + id: "101", + }, + ], + }, + "kokyu nage": { + "single-direction": [ + { + title: "tachi waza ushiro ryote dori kokyu nage 01", + videoId: "lSXiuN7ZKvo", + durationSeconds: 16, + id: "102", + }, + { + title: "tachi waza ushiro ryote dori kokyu nage 02", + videoId: "NLDzaKoOyHA", + durationSeconds: 18, + id: "103", + }, + { + title: "tachi waza ushiro ryote dori kokyu nage 03", + videoId: "3sRvYDaQN9E", + durationSeconds: 13, + id: "104", + }, + ], + }, + "juji garami": { + "single-direction": [ + { + title: "tachi waza ushiro ryote dori juji garami", + videoId: "9qgS2wQ0Z40", + durationSeconds: 23, + id: "214", + }, + ], + }, + "sokumen irimi nage": { + "single-direction": [ + { + title: "tachi waza ushiro ryote dori sokumen irimi nage", + videoId: "XF0KeGY32vM", + durationSeconds: 27, + id: "215", + }, + ], + }, + "shiho nage": { + omote: [ + { + title: "tachi waza ushiro ryote dori shiho nage omote", + videoId: "nSRq3OFu400", + durationSeconds: 26, + id: "216", + }, + ], + ura: [ + { + title: "tachi waza ushiro ryote dori shiho nage ura", + videoId: "QBRs9EECPk4", + durationSeconds: 24, + id: "217", + }, + ], + }, + "ude kime nage": { + "single-direction": [ + { + title: "tachi waza ushiro ryote dori ude kime nage omote", + videoId: "OImkO0mXc6Y", + durationSeconds: 24, + id: "218", + }, + ], + }, + }, + "katate ryote dori": { + ikkyo: { + omote: [ + { + title: "tachi waza katate ryote dori ikkyo omote", + videoId: "zNz8cIlvCEY", + durationSeconds: 30, + id: "71", + }, + ], + ura: [ + { + title: "tachi waza katate ryote dori ikkyo ura", + videoId: "5RwHTHBPoPc", + durationSeconds: 28, + id: "72", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza katate ryote dori nikyo omote", + videoId: "0sCP4X6nWwI", + durationSeconds: 34, + id: "73", + }, + ], + ura: [ + { + title: "tachi waza katate ryote dori nikyo ura", + videoId: "xSYZ2SpIVSA", + durationSeconds: 33, + id: "74", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza katate ryote dori kote gaeshi", + videoId: "9g0usMCj3i4", + durationSeconds: 33, + id: "75", + }, + ], + }, + "ude kime nage": { + omote: [ + { + title: "tachi waza katate ryote dori ude kime nage omote", + videoId: "N6_mLTPRkFU", + durationSeconds: 15, + id: "76", + }, + ], + }, + "sokumen irimi nage": { + "single-direction": [ + { + title: "tachi waza katate ryote dori sokumen irimi nage", + videoId: "NsWp_lc5wkg", + durationSeconds: 28, + id: "77", + }, + ], + }, + "kokyu nage": { + "single-direction": [ + { + title: "tachi waza katate ryote dori kokyu nage 01a", + videoId: "B5Vyd3jQhqk", + durationSeconds: 14, + id: "78", + }, + { + title: "tachi waza katate ryote dori kokyu nage 01b", + videoId: "zyMmaHK0Fz8", + durationSeconds: 23, + id: "79", + }, + { + title: "tachi waza katate ryote dori kokyu nage 02", + videoId: "OCFMAkIRX74", + durationSeconds: 13, + id: "80", + }, + ], + }, + "shiho nage": { + "single-direction": [ + { + title: "tachi waza katate ryote dori shiho nage omote 01", + videoId: "G032IIQHkEY", + durationSeconds: 30, + id: "211", + }, + { + title: "tachi waza katate ryote dori shiho nage omote 02", + videoId: "2tGPHY2iSXA", + durationSeconds: 22, + id: "212", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "tachi waza katate ryote dori irimi nage", + videoId: "3tZ3FVZW2cA", + durationSeconds: 26, + id: "213", + }, + ], + }, + "juji garami": { + "single-direction": [ + { + title: "tachi waza katate ryote dori juji garami", + videoId: "xjT9HgGPoFg", + durationSeconds: 16, + id: "281", + }, + ], + }, + }, + "kata dori men uchi": { + ikkyo: { + omote: [ + { + title: "tachi waza kata dori men uchi ikkyo omote", + videoId: "LK0yjnttxU0", + durationSeconds: 34, + id: "161", + }, + ], + ura: [ + { + title: "tachi waza kata dori men uchi ikkyo ura", + videoId: "U-nvtIB0J30", + durationSeconds: 30, + id: "162", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza kata dori men uchi nikyo omote", + videoId: "rthpDpijghk", + durationSeconds: 36, + id: "163", + }, + ], + ura: [ + { + title: "tachi waza kata dori men uchi nikyo ura", + videoId: "PnwuCyh2FW4", + durationSeconds: 34, + id: "164", + }, + ], + }, + sankyo: { + omote: [ + { + title: "tachi waza kata dori men uchi sankyo omote", + videoId: "qKspH3eZ8R0", + durationSeconds: 38, + id: "165", + }, + ], + ura: [ + { + title: "tachi waza kata dori men uchi sankyo ura", + videoId: "34yvXnCg1_Y", + durationSeconds: 39, + id: "166", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "tachi waza kata dori men uchi yonkyo omote", + videoId: "lhAPM4uqq7E", + durationSeconds: 30, + id: "167", + }, + ], + ura: [ + { + title: "tachi waza kata dori men uchi yonkyo ura", + videoId: "WtPHBWJzCYI", + durationSeconds: 37, + id: "168", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza kata dori men uchi kote gaeshi", + videoId: "GwoMiN2s640", + durationSeconds: 34, + id: "169", + }, + ], + }, + "shiho nage": { + "single-direction": [ + { + title: "tachi waza kata dori men uchi shiho nage", + videoId: "YYTNE0Yf6aw", + durationSeconds: 23, + id: "170", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "tachi waza kata dori men uchi irimi nage", + videoId: "spD7BbD4WQc", + durationSeconds: 30, + id: "171", + }, + ], + }, + "kokyu nage": { + "single-direction": [ + { + title: "tachi waza kata dori men uchi kokyu nage", + videoId: "8mMhDSJFEaw", + durationSeconds: 22, + id: "172", + }, + ], + }, + }, + "ushiro ryo kata dori": { + ikkyo: { + omote: [ + { + title: "tachi waza ushiro ryo kata dori ikkyo omote", + videoId: "hyYA3WH72_Y", + durationSeconds: 32, + id: "173", + }, + ], + ura: [ + { + title: "tachi waza ushiro ryo kata dori ikkyo ura", + videoId: "GyiubQCwaWE", + durationSeconds: 30, + id: "174", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza ushiro ryo kata dori nikyo omote", + videoId: "DzcKU9fHXSE", + durationSeconds: 34, + id: "175", + }, + ], + ura: [ + { + title: "tachi waza ushiro ryo kata dori nikyo ura", + videoId: "usC173DfIFg", + durationSeconds: 36, + id: "176", + }, + ], + }, + sankyo: { + omote: [ + { + title: "tachi waza ushiro ryo kata dori sankyo omote", + videoId: "iDGteTTBj-E", + durationSeconds: 36, + id: "177", + }, + ], + ura: [ + { + title: "tachi waza ushiro ryo kata dori sankyo ura", + videoId: "zm9nVV9dQiU", + durationSeconds: 36, + id: "178", + }, + ], + }, + "sokumen irimi nage": { + "single-direction": [ + { + title: "tachi waza ushiro ryo kata dori sokumen irimi nage 01", + videoId: "J6H4cH6c3rw", + durationSeconds: 27, + id: "179", + }, + { + title: "tachi waza ushiro ryo kata dori sokumen irimi nage 02", + videoId: "ON_-LcZvGNQ", + durationSeconds: 26, + id: "180", + }, + ], + }, + }, + "chudan tsuki": { + ikkyo: { + omote: [ + { + title: "tachi waza chudan tsuki ikkyo omote", + videoId: "-PWD3x1qi-0", + durationSeconds: 26, + id: "219", + }, + ], + ura: [ + { + title: "tachi waza chudan tsuki ikkyo ura", + videoId: "ohdLSy3Q1go", + durationSeconds: 21, + id: "220", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "tachi waza chudan tsuki irimi nage", + videoId: "vm6Qze7etog", + durationSeconds: 25, + id: "221", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza chudan tsuki nikyo omote", + videoId: "zLvwioNWjpY", + durationSeconds: 27, + id: "262", + }, + ], + ura: [ + { + title: "tachi waza chudan tsuki nikyo ura", + videoId: "hx4IBBwkUlA", + durationSeconds: 31, + id: "263", + }, + ], + }, + sankyo: { + omote: [ + { + title: "tachi waza chudan tsuki sankyo omote", + videoId: "1VlrLxkFWOY", + durationSeconds: 31, + id: "264", + }, + ], + ura: [ + { + title: "tachi waza chudan tsuki sankyo ura", + videoId: "rsqoIEMPqfs", + durationSeconds: 30, + id: "265", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "tachi waza chudan tsuki yonkyo omote", + videoId: "EndWQYJjknI", + durationSeconds: 34, + id: "266", + }, + ], + ura: [ + { + title: "tachi waza chudan tsuki yonkyo ura", + videoId: "zCOHMhtwkdc", + durationSeconds: 22, + id: "267", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza chudan tsuki kote gaeshi", + videoId: "8OFsmMhLQJk", + durationSeconds: 27, + id: "268", + }, + ], + }, + "soto kaiten nage": { + omote: [ + { + title: "tachi waza chudan tsuki soto kaiten nage omote", + videoId: "imYcd3r0sSk", + durationSeconds: 13, + id: "269", + }, + ], + ura: [ + { + title: "tachi waza chudan tsuki soto kaiten nage ura", + videoId: "kZIBjHsNCI8", + durationSeconds: 12, + id: "270", + }, + ], + }, + "uchi kaiten nage": { + omote: [ + { + title: "tachi waza chudan tsuki uchi kaiten nage omote", + videoId: "rKkZRJ4Z5Ik", + durationSeconds: 23, + id: "271", + }, + ], + ura: [ + { + title: "tachi waza chudan tsuki uchi kaiten nage ura", + videoId: "gnhl1MA4z8U", + durationSeconds: 17, + id: "272", + }, + ], + }, + }, + "jodan tsuki": { + "hiji kimo osae": { + "single-direction": [ + { + title: "tachi waza jodan tsuki hiji kime osae", + videoId: "4NEEUNKpmN8", + durationSeconds: 23, + id: "222", + }, + ], + }, + ikkyo: { + omote: [ + { + title: "tachi waza jodan tsuki ikkyo omote", + videoId: "5vQf-7ZpoL4", + durationSeconds: 16, + id: "248", + }, + ], + ura: [ + { + title: "tachi waza jodan tsuki ikkyo ura", + videoId: "zZSppq9GrRw", + durationSeconds: 11, + id: "249", + }, + ], + }, + nikyo: { + omote: [ + { + title: "tachi waza jodan tsuki nikyo omote", + videoId: "-SAcdtRsAuE", + durationSeconds: 20, + id: "250", + }, + ], + ura: [ + { + title: "tachi waza jodan tsuki nikyo ura", + videoId: "Asn8X7AZIk0", + durationSeconds: 23, + id: "251", + }, + ], + }, + sankyo: { + omote: [ + { + title: "tachi waza jodan tsuki sankyo omote", + videoId: "qHVKjktps3Q", + durationSeconds: 29, + id: "252", + }, + ], + ura: [ + { + title: "tachi waza jodan tsuki sankyo ura", + videoId: "wvvFzoS3vaU", + durationSeconds: 24, + id: "253", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "tachi waza jodan tsuki yonkyo omote", + videoId: "4lWtwVfYLfg", + durationSeconds: 17, + id: "254", + }, + ], + ura: [ + { + title: "tachi waza jodan tsuki yonkyo ura", + videoId: "CQ5pIFTXm8A", + durationSeconds: 16, + id: "255", + }, + ], + }, + "shiho nage": { + omote: [ + { + title: "tachi waza jodan tsuki shiho nage omote", + videoId: "88hd8_dmfa0", + durationSeconds: 22, + id: "256", + }, + ], + }, + "irimi nage": { + "single-direction": [ + { + title: "tachi waza jodan tsuki irimi nage", + videoId: "6Ju8QDHLP6Q", + durationSeconds: 28, + id: "257", + }, + ], + }, + "soto kaiten nage": { + omote: [ + { + title: "tachi waza jodan tsuki soto kaiten nage omote", + videoId: "MDp6wZMpj5c", + durationSeconds: 15, + id: "258", + }, + ], + ura: [ + { + title: "tachi waza jodan tsuki soto kaiten nage ura", + videoId: "LdMPeDHB5wY", + durationSeconds: 19, + id: "259", + }, + ], + }, + "uchi kaiten nage": { + omote: [ + { + title: "tachi waza jodan tsuki uchi kaiten nage omote", + videoId: "ODmwo1i9GR8", + durationSeconds: 15, + id: "260", + }, + ], + ura: [ + { + title: "tachi waza jodan tsuki uchi kaiten nage ura", + videoId: "0VRvUccgHZ8", + durationSeconds: 10, + id: "261", + }, + ], + }, + }, + "ushiro ryo hiji dori": { + ikkyo: { + omote: [ + { + title: "tachi waza ushiro ryo hiji dori ikkyo omote", + videoId: "Ci_cb5tlyb0", + durationSeconds: 30, + id: "223", + }, + ], + ura: [ + { + title: "tachi waza ushiro ryo hiji dori ikkyo ura", + videoId: "Tb8wipTVG-4", + durationSeconds: 32, + id: "224", + }, + ], + }, + "shiho nage": { + omote: [ + { + title: "tachi waza ushiro ryo hiji dori shiho nage omote", + videoId: "cD5UKDzptJM", + durationSeconds: 10, + id: "275", + }, + ], + ura: [ + { + title: "tachi waza ushiro ryo hiji dori shiho nage ura", + videoId: "UO6VZLKI7lw", + durationSeconds: 8, + id: "276", + }, + ], + }, + }, + "mae ryo kata dori": { + "sokumen irimi nage": { + "single-direction": [ + { + title: "tachi waza mae ryo kata dori sokumen irimi nage", + videoId: "uec9796snuY", + durationSeconds: 22, + id: "225", + }, + ], + }, + }, + "ushiro eri dori": { + ikkyo: { + omote: [ + { + title: "tachi waza ushiro eri dori ikkyo omote", + videoId: "JDYDPUcwbIA", + durationSeconds: 26, + id: "244", + }, + ], + ura: [ + { + title: "tachi waza ushiro eri dori ikkyo ura", + videoId: "81ej6LvdkvA", + durationSeconds: 24, + id: "245", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza ushiro eri dori kote gaeshi", + videoId: "lrBvKPIr4Fw", + durationSeconds: 26, + id: "246", + }, + ], + }, + "shiho nage": { + "single-direction": [ + { + title: "tachi waza ushiro eri dori shiho nage", + videoId: "G5DdZJS0Jj0", + durationSeconds: 34, + id: "247", + }, + ], + }, + }, + "muna dori": { + "shiho nage": { + "single-direction": [ + { + title: "tachi waza muna dori shiho nage", + videoId: "k0K2IjYKs_Y", + durationSeconds: 19, + id: "274", + }, + ], + }, + }, + "ushiro katate dori kubi shime": { + ikkyo: { + omote: [ + { + title: "tachi waza ushiro katate dori kubi shime ikkyo omote", + videoId: "RrusD4gyxMg", + durationSeconds: 20, + id: "277", + }, + ], + ura: [ + { + title: "tachi waza ushiro katate dori kubi shime ikkyo ura", + videoId: "hJgtiKHCYJE", + durationSeconds: 19, + id: "278", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "tachi waza ushiro katate dori kubi shime kote gaeshi", + videoId: "6qzK2vnNB7I", + durationSeconds: 24, + id: "279", + }, + ], + }, + "shiho nage": { + "single-direction": [ + { + title: "tachi waza ushiro katate dori kubi shime shiho nage", + videoId: "HiLBy7oPDIc", + durationSeconds: 30, + id: "280", + }, + ], + }, + }, + }, + "hanmi handachi waza": { + "gyuako hanmi katate dori": { + "shiho nage": { + omote: [ + { + title: "hanmi handachi waza katate dori shiho nage omote", + videoId: "Y2Ka7Zjpt1Q", + durationSeconds: 19, + id: "66", + }, + ], + ura: [ + { + title: "hanmi handachi waza katate dori shiho nage ura", + videoId: "fnAacJCkSAE", + durationSeconds: 16, + id: "67", + }, + ], + }, + "uchi kaiten nage": { + omote: [ + { + title: "hanmi handachi waza katate dori uchi kaiten nage omote", + videoId: "buCpYcLmYlA", + durationSeconds: 22, + id: "68", + }, + ], + ura: [ + { + title: "hanmi handachi waza katate dori uchi kaiten nage ura", + videoId: "jk9nnXxYJ4w", + durationSeconds: 22, + id: "69", + }, + ], + }, + "soto kaiten nage": { + omote: [ + { + title: "hanmi handachi waza katate dori soto kaiten nage omote", + videoId: "uyO9Lti4Mj0", + durationSeconds: 22, + id: "159", + }, + ], + ura: [ + { + title: "hanmi handachi waza katate dori soto kaiten nage ura", + videoId: "wkm2I-aditY", + durationSeconds: 23, + id: "160", + }, + ], + }, + ikkyo: { + omote: [ + { + title: "hanmi handachi waza katate dori ikkyo omote", + videoId: "RpANzuvl-EU", + durationSeconds: 25, + id: "233", + }, + ], + ura: [ + { + title: "hanmi handachi waza katate dori ikkyo ura", + videoId: "25A191PltAg", + durationSeconds: 22, + id: "234", + }, + ], + }, + nikyo: { + omote: [ + { + title: "hanmi handachi waza katate dori nikyo omote", + videoId: "diH69ItzFps", + durationSeconds: 22, + id: "235", + }, + ], + ura: [ + { + title: "hanmi handachi waza katate dori nikyo ura", + videoId: "NF89e-sIzRU", + durationSeconds: 35, + id: "236", + }, + ], + }, + }, + "shomen uchi": { + "irimi nage": { + "single-direction": [ + { + title: "hanmi handachi waza shomen uchi irimi nage", + videoId: "1KahuOB4TIU", + durationSeconds: 20, + id: "70", + }, + ], + }, + "kote gaeshi": { + "single-direction": [ + { + title: "hanmi handachi waza shomen uchi kote gaeshi", + videoId: "spqR3I4_ZYA", + durationSeconds: 29, + id: "156", + }, + ], + }, + ikkyo: { + omote: [ + { + title: "hanmi handachi waza shomen uchi ikkyo omote", + videoId: "R37nt6DYPxE", + durationSeconds: 19, + id: "228", + }, + ], + ura: [ + { + title: "hanmi handachi waza shomen uchi ikkyo ura", + videoId: "9Jn39xlD7lc", + durationSeconds: 16, + id: "229", + }, + ], + }, + nikyo: { + omote: [ + { + title: "hanmi handachi waza shomen uchi nikyo omote", + videoId: "6hIP-5ubeiM", + durationSeconds: 21, + id: "230", + }, + ], + ura: [ + { + title: "hanmi handachi waza shomen uchi nikyo ura", + videoId: "iRNwxzRupa0", + durationSeconds: 32, + id: "231", + }, + ], + }, + gokyo: { + "single-direction": [ + { + title: "hanmi handachi waza shomen uchi gokyo", + videoId: "ucC45Xl1iF0", + durationSeconds: 15, + id: "232", + }, + ], + }, + }, + "ryote dori": { + "shiho nage": { + omote: [ + { + title: "hanmi handachi waza ryote dori shiho nage omote", + videoId: "QOW-QUUZv3E", + durationSeconds: 29, + id: "157", + }, + ], + ura: [ + { + title: "hanmi handachi waza ryote dori shiho nage ura", + videoId: "AD6DQSUKCIE", + durationSeconds: 14, + id: "158", + }, + ], + }, + }, + "ushiro ryo kata dori": { + ikkyo: { + omote: [ + { + title: "hanmi handachi waza ushiro ryo kata dori ikkyo omote", + videoId: "cIWZ1XyvDk8", + durationSeconds: 21, + id: "237", + }, + ], + ura: [ + { + title: "hanmi handachi waza ushiro ryo kata dori ikkyo ura", + videoId: "TD4Ial8vZQk", + durationSeconds: 20, + id: "238", + }, + ], + }, + nikyo: { + omote: [ + { + title: "hanmi handachi waza ushiro ryo kata dori nikyo omote", + videoId: "g26L8digd9Y", + durationSeconds: 25, + id: "239", + }, + ], + }, + sankyo: { + omote: [ + { + title: "hanmi handachi waza ushiro ryo kata dori sankyo omote", + videoId: "8STsvJ8zT-c", + durationSeconds: 24, + id: "240", + }, + ], + ura: [ + { + title: "hanmi handachi waza ushiro ryo kata dori sankyo ura", + videoId: "JWuoF9cWYYI", + durationSeconds: 52, + id: "241", + }, + ], + }, + yonkyo: { + omote: [ + { + title: "hanmi handachi waza ushiro ryo kata dori yonkyo omote", + videoId: "3wpWxLW8_y8", + durationSeconds: 22, + id: "242", + }, + ], + ura: [ + { + title: "hanmi handachi waza ushiro ryo kata dori yonkyo ura", + videoId: "iaWIbQVqBhE", + durationSeconds: 11, + id: "243", + }, + ], + }, + }, + }, + }, +} satisfies VideoPack; diff --git a/src/data/videopacks/index.test.ts b/src/data/videopacks/index.test.ts new file mode 100644 index 0000000..6a8635f --- /dev/null +++ b/src/data/videopacks/index.test.ts @@ -0,0 +1,11 @@ +import { listVideoPacks } from "@/data/videopacks/index.ts"; + +describe("videoPacks", () => { + it("listVideoPacks", async () => { + expect(await listVideoPacks()).toContainEqual({ + id: "aikido-kompendium", + name: "Aikido Kompendium", + source: "https://www.aikido-kompendium.de", + }); + }); +}); diff --git a/src/data/videopacks/index.ts b/src/data/videopacks/index.ts new file mode 100644 index 0000000..61430a0 --- /dev/null +++ b/src/data/videopacks/index.ts @@ -0,0 +1,26 @@ +import type { VideoPack } from "$core/model/VideoPack.ts"; + +export interface VideoPackInfo { + id: string; + name: string; + source: string; +} + +export async function listVideoPacks(): Promise { + const videoPacks = import.meta.glob("./*/index.ts", { import: "default" }); + return Promise.all( + Object.entries(videoPacks).map(async ([url, videoPack]) => { + const id = url.replace(/\.\/(.*)\/index\.ts/, "$1"); + const pack = await videoPack(); + return { + id, + name: pack.name, + source: pack.source, + }; + }), + ); +} + +export async function loadVideoPack(id: string): Promise { + return (await import(`./${id}/index.ts`)).default; +} diff --git a/src/i18n/common/de.json b/src/i18n/common/de.json index 40b4570..cf9c743 100644 --- a/src/i18n/common/de.json +++ b/src/i18n/common/de.json @@ -16,6 +16,7 @@ "donations.liberapay.label": "Unterstütze mich mit Liberapay", "donations.paypal.label": "Spendiere einen Kaffee per Paypal", "donations.question": "Hat diese App dir geholfen?", + "editVideoPack.title": "Video-Pack bearbeiten", "examChooser.created-by": "Diese Prüfungstabelle wurde mit \"Aikido-Prüfung\" ({# url #}) erzeugt.", "examChooser.exams-from": "Prüfungen von", "examChooser.exams.header": "Prüfungen", diff --git a/src/i18n/common/en.json b/src/i18n/common/en.json index ce1a1be..9801193 100644 --- a/src/i18n/common/en.json +++ b/src/i18n/common/en.json @@ -16,6 +16,7 @@ "donations.liberapay.label": "Support me at Liberapay", "donations.paypal.label": "Buy me a coffee at PayPal", "donations.question": "Did this app help you?", + "editVideoPack.title": "Edit video pack", "examChooser.created-by": "This exam table was created with \"Aikido-Exam\" at {# url #}", "examChooser.exams-from": "Exams from", "examChooser.exams.header": "Exams", diff --git a/src/pages/[language]/videos/[videoPackId]/index.astro b/src/pages/[language]/videos/[videoPackId]/index.astro new file mode 100644 index 0000000..5c3d8dd --- /dev/null +++ b/src/pages/[language]/videos/[videoPackId]/index.astro @@ -0,0 +1,30 @@ +--- +import { listVideoPacks, loadVideoPack } from "@/data/videopacks"; + +import DefaultLayout from "@/layouts/DefaultLayout.astro"; +import Header from "@/components/astro/Header.astro"; +import { t, tx } from "@/i18n"; + +import { multiplyParams } from "@/utils/multiplyParams"; +import { languages } from "@/i18n/server"; +import { VideoPackEditor } from "@/components/solid/organisms/VideoPackEditor"; + +export async function getStaticPaths() { + const videoPacks = await listVideoPacks(); + return multiplyParams({ + language: languages, + videoPackId: videoPacks.map((pack) => pack.id), + }); +} + +const id = Astro.params.videoPackId; +const videoPack = await loadVideoPack(id); +--- + + +
+ +

{t("editVideoPack.title")}

+ + diff --git a/src/pages/[language]/videos/index.astro b/src/pages/[language]/videos/index.astro new file mode 100644 index 0000000..9e73722 --- /dev/null +++ b/src/pages/[language]/videos/index.astro @@ -0,0 +1,29 @@ +--- +import { listVideoPacks } from "../../../data/videopacks"; +import DefaultLayout from "@/layouts/DefaultLayout.astro"; +import Header from "@/components/astro/Header.astro"; +import { t, tx } from "@/i18n"; +import { LinkButton } from "@/components/solid/atoms/LinkButton"; + +import { multiplyParams } from "@/utils/multiplyParams"; +import { languages } from "@/i18n/server"; + +export async function getStaticPaths() { + return multiplyParams({ + language: languages, + }); +} + +const videoPacks = await listVideoPacks(); +--- + + +
VideoPacks
+
+ { + videoPacks.map((videoPack) => { + return ; + }) + } +
+