├── infra ├── .gitignore ├── playbooks │ ├── cluster │ │ ├── tests │ │ │ ├── inventory │ │ │ └── test.yml │ │ ├── vars │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ ├── defaults │ │ │ └── main.yml │ │ └── handlers │ │ │ └── main.yml │ ├── requirements.txt │ ├── requirements.yml │ ├── default-secrets.yml │ ├── files │ │ ├── rook-ceph-values.yaml │ │ ├── prometheus-postgres-exporter.yaml │ │ ├── elastic-values.yaml │ │ ├── zfs-monitoring.yaml │ │ └── kibana-ingress.yaml │ ├── external.yaml │ ├── HLS_PGM.yml │ ├── templates │ │ ├── database │ │ │ ├── servicemonitor.yaml │ │ │ └── nfs-backup-volume.yaml │ │ ├── beta │ │ │ ├── media │ │ │ │ ├── ingress.yaml │ │ │ │ └── storage.yaml │ │ │ ├── jukebox.yaml │ │ │ └── epg.yaml │ │ └── telegram-bot.yaml │ ├── rook-ceph.yml │ └── README.md ├── old │ ├── roles │ │ ├── playout │ │ │ ├── meta │ │ │ │ └── main.yml │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ │ ├── fk-get-schedule-from-backend.timer.j2 │ │ │ │ ├── fk-get-filler-videos.timer.j2 │ │ │ │ ├── fk-get-schedule-from-backend.service.j2 │ │ │ │ ├── fk-playout.service.j2 │ │ │ │ ├── mnt-media.mount.j2 │ │ │ │ ├── fk-get-filler-videos.service.j2 │ │ │ │ └── update.githook.j2 │ │ ├── upload-app │ │ │ ├── meta │ │ │ │ └── main.yml │ │ │ ├── defaults │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ │ ├── move_and_process.service.j2 │ │ │ │ ├── fkupload.service.j2 │ │ │ │ ├── apache.conf.j2 │ │ │ │ └── nginx.conf.j2 │ │ ├── debian_stock_config │ │ │ ├── vars │ │ │ │ ├── packages.yml │ │ │ │ └── hosts.yml │ │ │ ├── tasks │ │ │ │ ├── hosts.yml │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ │ └── etc │ │ │ │ └── hosts.j2 │ │ └── common │ │ │ └── tasks │ │ │ └── main.yml │ ├── group_vars │ │ └── all │ │ │ ├── main.yml │ │ │ └── upload.yml │ ├── k8s-beta │ │ ├── README.md │ │ ├── migrate-from-fkweb-nightly.yaml │ │ └── schedule.yaml │ ├── k8s │ │ ├── streaming │ │ │ ├── 000-namespace.yaml │ │ │ ├── 003-service.yaml │ │ │ ├── 001-config.yaml │ │ │ ├── 002-config.yaml │ │ │ └── 003-ingress.yaml │ │ ├── scratchpad.sh │ │ ├── README.md │ │ ├── multiviewer-streaming │ │ │ ├── 000-namespace.yaml │ │ │ ├── 003-service.yaml │ │ │ ├── 002-config.yaml │ │ │ ├── 001-config.yaml │ │ │ └── 003-ingress.yaml │ │ ├── ingress │ │ │ ├── service_kiloview.yaml │ │ │ ├── servicemonitor.yaml │ │ │ ├── ingress_kiloview.yaml │ │ │ ├── old │ │ │ │ ├── 005-ingress.yaml │ │ │ │ ├── 004-middleware.yaml │ │ │ │ ├── 001-persistent-cert-storage.yaml │ │ │ │ ├── 003-service.yaml │ │ │ │ └── 001-rbac.yaml │ │ │ ├── frikanalen_middlewares.yaml │ │ │ └── kiloview.yaml │ │ ├── oven-media-engine │ │ │ └── README │ │ ├── stills-uploader │ │ │ ├── 002-service.yaml │ │ │ ├── 001-deployment.yaml │ │ │ └── 003-ingress.yaml │ │ ├── monitoring │ │ │ ├── stream-monitor │ │ │ │ ├── 002-service.yaml │ │ │ │ └── 001-deployment.yaml │ │ │ ├── grafana-ingress.yaml │ │ │ ├── prom-ingress.yaml │ │ │ ├── alertmanager.yaml │ │ │ ├── kube_alerts.yaml │ │ │ └── junos_exporter │ │ │ │ └── README.md │ │ ├── stills-generator │ │ │ ├── 002-service.yaml │ │ │ ├── 001-deployment.yaml │ │ │ └── 003-ingress.yaml-probably-deprecated │ │ ├── debug │ │ │ └── 001-dnsutils.yaml │ │ ├── helm-values │ │ │ ├── prometheus-postgres-exporter-beta.yaml │ │ │ ├── prometheus-postgres-exporter.yaml │ │ │ └── README │ │ ├── ingest │ │ │ └── upload-processor-legacy │ │ │ │ ├── config.yaml │ │ │ │ └── 001-deployment.yaml │ │ ├── rook-ceph │ │ │ ├── dashboard-service.yaml │ │ │ ├── service-monitor.yaml │ │ │ ├── set-policy.bash │ │ │ ├── s3-service.yaml │ │ │ └── ingress.yaml │ │ ├── atem-control │ │ │ ├── 002-service.yaml │ │ │ └── 001-deployment.yaml │ │ ├── playout │ │ │ ├── playout-primary.yaml │ │ │ ├── schedule-service.yaml │ │ │ └── playout-secondary.yaml │ │ ├── graphics │ │ │ └── graphics.yaml │ │ └── frontend │ │ │ └── frontend.yaml │ ├── k8s-dev │ │ ├── config-frontend.yaml │ │ ├── dummy_atem.yaml │ │ ├── dummy_frontend.yaml │ │ ├── dummy_upload_receiver.yaml │ │ ├── coredns-config.yaml │ │ ├── block-storage-class.yaml │ │ └── s3-emulator.yaml │ ├── k8s-legacy │ │ ├── config-frontend.yaml │ │ ├── s3-alias.yaml │ │ ├── ingress-frontend.yaml │ │ ├── ingress-upload-receiver.yaml │ │ ├── nfs-db-backup.yaml │ │ ├── ingress-graphics.yaml │ │ ├── tmp-acme-pv.yaml │ │ └── tmp-db-pv.yaml │ ├── hosts │ ├── charts │ │ └── frontend │ │ │ ├── templates │ │ │ ├── service.yaml │ │ │ ├── serviceaccount.yaml │ │ │ ├── tests │ │ │ │ └── test-connection.yaml │ │ │ ├── ingress.yaml │ │ │ ├── hpa.yaml │ │ │ └── deployment.yaml │ │ │ ├── .helmignore │ │ │ ├── values.yaml │ │ │ └── Chart.yaml │ └── site.yml ├── .vscode │ └── settings.json ├── README └── argocd │ └── django-backend.yaml ├── packages ├── playout │ ├── .gitignore │ ├── requirements.txt │ ├── cache │ │ └── dailyplan │ │ │ └── plan20200205.pickle │ └── Dockerfile ├── frontend │ ├── .prettierrc.json │ ├── modules │ │ ├── input │ │ │ ├── constants.ts │ │ │ └── components │ │ │ │ ├── ControlledDropdownInput.tsx │ │ │ │ ├── ControlledTextInput.tsx │ │ │ │ └── FileInput.tsx │ │ ├── playout │ │ │ ├── types.ts │ │ │ ├── constants.ts │ │ │ ├── forms │ │ │ │ └── createTextSlideForm.ts │ │ │ ├── helpers │ │ │ │ └── spawnTextSlideModal.ts │ │ │ └── components │ │ │ │ └── ATEMControls.tsx │ │ ├── form │ │ │ ├── types │ │ │ │ └── Validator.ts │ │ │ ├── hooks │ │ │ │ ├── useForm.ts │ │ │ │ └── useField.ts │ │ │ ├── helpers │ │ │ │ ├── checkIfFieldIsReady.ts │ │ │ │ └── checkFieldMeta.ts │ │ │ ├── components │ │ │ │ ├── FieldsProvider.tsx │ │ │ │ ├── FormField.tsx │ │ │ │ └── Form.tsx │ │ │ └── fields │ │ │ │ └── array.ts │ │ ├── lang │ │ │ ├── async.ts │ │ │ ├── types.ts │ │ │ ├── array.ts │ │ │ ├── number.ts │ │ │ └── string.ts │ │ ├── ui │ │ │ ├── types.ts │ │ │ ├── styles │ │ │ │ └── linkStyle.ts │ │ │ ├── components │ │ │ │ ├── Document.tsx │ │ │ │ ├── SVGIcon.tsx │ │ │ │ ├── ExternalLink.tsx │ │ │ │ ├── InternalLink.tsx │ │ │ │ ├── IconButton.tsx │ │ │ │ ├── ButtonList.tsx │ │ │ │ ├── Quote.tsx │ │ │ │ ├── Spinner.tsx │ │ │ │ ├── EmptyState.tsx │ │ │ │ ├── Button.tsx │ │ │ │ ├── ScrollTrigger.tsx │ │ │ │ └── Section.tsx │ │ │ └── hooks │ │ │ │ ├── useWindowEvent.ts │ │ │ │ ├── useAnimation.ts │ │ │ │ ├── useStatusLine.ts │ │ │ │ └── useInterpolatedValue.ts │ │ ├── state │ │ │ ├── helpers │ │ │ │ ├── getUniqueId.ts │ │ │ │ └── interpretError.ts │ │ │ ├── lists.ts │ │ │ ├── hooks │ │ │ │ ├── useObserver.ts │ │ │ │ ├── useCookie.ts │ │ │ │ └── useResourceList.ts │ │ │ ├── types.ts │ │ │ ├── classes │ │ │ │ ├── Store.ts │ │ │ │ └── Resource.ts │ │ │ ├── components │ │ │ │ └── ListTail.tsx │ │ │ └── manager.ts │ │ ├── network │ │ │ ├── types.ts │ │ │ ├── constants.ts │ │ │ └── config.ts │ │ ├── modal │ │ │ ├── components │ │ │ │ ├── PrimaryModal │ │ │ │ │ ├── Body.tsx │ │ │ │ │ ├── Footer.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── Actions.tsx │ │ │ │ │ ├── Header.tsx │ │ │ │ │ └── Container.tsx │ │ │ │ └── ModalOverlay.tsx │ │ │ ├── contexts.ts │ │ │ └── hooks │ │ │ │ └── useModal.ts │ │ ├── organization │ │ │ ├── helpers │ │ │ │ ├── formatAddress.ts │ │ │ │ └── fetchBrregData.ts │ │ │ ├── forms │ │ │ │ └── createNewOrganizationForm.ts │ │ │ └── resources │ │ │ │ └── Organization.ts │ │ ├── popover │ │ │ ├── components │ │ │ │ └── PrimaryPopover.tsx │ │ │ ├── types │ │ │ │ └── Popover.ts │ │ │ ├── contexts.ts │ │ │ ├── hooks │ │ │ │ └── usePopoverContext.ts │ │ │ └── stores │ │ │ │ └── popoverStore.ts │ │ ├── core │ │ │ ├── constants.ts │ │ │ ├── components │ │ │ │ ├── NavLinks.tsx │ │ │ │ ├── Body.tsx │ │ │ │ ├── AspectContainer.tsx │ │ │ │ ├── HeaderAuthBar.tsx │ │ │ │ └── Footer.tsx │ │ │ └── styles │ │ │ │ └── mainContentStyle.ts │ │ ├── user │ │ │ ├── schemas.ts │ │ │ └── forms │ │ │ │ └── createProfileForm.ts │ │ ├── schedule │ │ │ ├── helpers │ │ │ │ ├── humanizeSelectedScheduleDate.ts │ │ │ │ └── humanizeScheduleItemDate.ts │ │ │ ├── resources │ │ │ │ └── ScheduleItem.ts │ │ │ └── components │ │ │ │ └── ScheduleItemBlurb.tsx │ │ ├── auth │ │ │ ├── helpers │ │ │ │ ├── spawnLoginModal.ts │ │ │ │ └── spawnRegisterModal.ts │ │ │ └── forms │ │ │ │ ├── createLoginForm.ts │ │ │ │ └── createRegisterForm.ts │ │ ├── video │ │ │ ├── types.ts │ │ │ ├── components │ │ │ │ ├── VideoPlayer.tsx │ │ │ │ ├── VideoGrid.tsx │ │ │ │ └── LiveVideoPlayer.tsx │ │ │ ├── forms │ │ │ │ └── createNewVideoForm.ts │ │ │ └── resources │ │ │ │ └── Video.ts │ │ └── styling │ │ │ ├── components │ │ │ └── ThemeContext.tsx │ │ │ └── themes.ts │ ├── pages │ │ ├── healthz.js │ │ └── organization │ │ │ └── [organizationId] │ │ │ └── plan.module.sass │ ├── public │ │ └── favicon.ico │ ├── .dockerignore │ ├── next-env.d.ts │ ├── types │ │ ├── next-env.d.ts │ │ ├── jsmpeg-player.d.ts │ │ └── emotion.d.ts │ ├── .yarnclean │ ├── README.md │ ├── Dockerfile │ ├── tsconfig.json │ └── .eslintrc.yml ├── schedule-service │ ├── .dockerignore │ ├── database │ │ ├── __init__.py │ │ └── foo.py │ ├── requirements.txt │ ├── README.md │ ├── Dockerfile │ ├── app.py │ └── playout.py └── utils │ ├── stills-generator │ ├── .gitignore │ ├── requirements.txt │ ├── Roboto-Black.ttf │ ├── background.png │ ├── Dockerfile │ ├── README.md │ ├── app.py │ └── chargen.py │ ├── ingest │ ├── copy-to-legacy │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── requirements.txt │ │ ├── Dockerfile │ │ ├── s3_policies.json │ │ └── README.md │ └── copy-to-cloudflare │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── config │ │ └── s3_endpoint_url │ │ ├── requirements.txt │ │ ├── Dockerfile │ │ └── s3_policies.json │ ├── prom-check-video-stream │ ├── .gitignore │ ├── requirements.txt │ ├── README.md │ ├── snapshot.py │ └── audio_analysis.py │ ├── upload-receiver │ ├── requirements.txt │ └── Dockerfile │ ├── monitoring-stream-server │ ├── .gitignore │ ├── .dockerignore │ ├── deploy.sh │ ├── tsconfig.json │ ├── README.md │ ├── Dockerfile │ ├── fk-multiviewer-stream.service │ └── package.json │ ├── atem-control │ ├── .env │ ├── .gitignore │ ├── .dockerignore │ ├── tsconfig.json │ ├── src │ │ ├── poster │ │ │ └── router.ts │ │ ├── auth │ │ │ ├── checkStaff.ts │ │ │ └── sessionV1.ts │ │ ├── atem │ │ │ ├── router.ts │ │ │ ├── AtemInterface.ts │ │ │ ├── MockAtem.ts │ │ │ └── utils.ts │ │ ├── logger.ts │ │ └── auth.ts │ ├── Dockerfile │ └── package.json │ ├── frostguard │ ├── requirements.txt │ └── Dockerfile │ ├── obe-service │ ├── fk-obe.conf │ ├── fk-obe.service │ └── README.md │ └── test-videos-generator │ └── mktestvid ├── .vscode └── settings.json ├── tests └── data │ ├── sine.wav │ ├── white.png │ └── white-sine.ogv ├── SECURITY.md ├── .gitignore ├── Makefile └── .github └── workflows ├── playout.yml ├── schedule-service.yml ├── atem-control-service.yml ├── stills-generator.yml └── monitoring-stream-server.yml /infra/.gitignore: -------------------------------------------------------------------------------- 1 | *.retry 2 | -------------------------------------------------------------------------------- /packages/playout/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | -------------------------------------------------------------------------------- /packages/frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/schedule-service/.dockerignore: -------------------------------------------------------------------------------- 1 | env/ 2 | -------------------------------------------------------------------------------- /packages/utils/stills-generator/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /infra/playbooks/cluster/tests/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | 3 | -------------------------------------------------------------------------------- /infra/playbooks/requirements.txt: -------------------------------------------------------------------------------- 1 | openshift==0.11.2 2 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-legacy/.dockerignore: -------------------------------------------------------------------------------- 1 | env 2 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-legacy/.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | -------------------------------------------------------------------------------- /infra/old/roles/playout/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-cloudflare/.dockerignore: -------------------------------------------------------------------------------- 1 | env 2 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-cloudflare/.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | -------------------------------------------------------------------------------- /packages/utils/prom-check-video-stream/.gitignore: -------------------------------------------------------------------------------- 1 | fk_test.ts 2 | -------------------------------------------------------------------------------- /packages/utils/upload-receiver/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | paramiko -------------------------------------------------------------------------------- /infra/old/group_vars/all/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | fk_media_dir: /tank/media/ 3 | -------------------------------------------------------------------------------- /infra/playbooks/cluster/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars file for cluster 3 | -------------------------------------------------------------------------------- /packages/utils/stills-generator/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | gunicorn 3 | -------------------------------------------------------------------------------- /infra/playbooks/cluster/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for cluster 3 | -------------------------------------------------------------------------------- /packages/utils/monitoring-stream-server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /infra/old/k8s-beta/README.md: -------------------------------------------------------------------------------- 1 | This contains the CRDs for beta.frikanalen.no. 2 | -------------------------------------------------------------------------------- /infra/playbooks/cluster/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for cluster 3 | -------------------------------------------------------------------------------- /infra/playbooks/cluster/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for cluster 3 | -------------------------------------------------------------------------------- /packages/schedule-service/database/__init__.py: -------------------------------------------------------------------------------- 1 | from .schedule import Schedule 2 | -------------------------------------------------------------------------------- /packages/utils/monitoring-stream-server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ansible.python.interpreterPath": "/bin/python" 3 | } 4 | -------------------------------------------------------------------------------- /packages/frontend/modules/input/constants.ts: -------------------------------------------------------------------------------- 1 | export const FIELDSET_HEIGHT = "38px"; 2 | -------------------------------------------------------------------------------- /packages/utils/atem-control/.env: -------------------------------------------------------------------------------- 1 | FK_API_URL=http: 2 | FK_APIV2_URL=http://localhost:8080 -------------------------------------------------------------------------------- /packages/utils/frostguard/requirements.txt: -------------------------------------------------------------------------------- 1 | #aiohttp[speedups] 2 | prometheus-client 3 | -------------------------------------------------------------------------------- /infra/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ansible.python.interpreterPath": "/bin/python" 3 | } -------------------------------------------------------------------------------- /infra/old/group_vars/all/upload.yml: -------------------------------------------------------------------------------- 1 | --- 2 | fk_auth_token: "{{ vault_fk_auth_token }}" 3 | -------------------------------------------------------------------------------- /infra/old/roles/upload-app/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - {role: 'nginx'} 4 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-cloudflare/config/s3_endpoint_url: -------------------------------------------------------------------------------- 1 | http://192.168.3.36:8085 2 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-legacy/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | kafka-python 3 | paramiko 4 | -------------------------------------------------------------------------------- /infra/old/roles/debian_stock_config/vars/packages.yml: -------------------------------------------------------------------------------- 1 | --- 2 | base_packages: 3 | - screen 4 | -------------------------------------------------------------------------------- /packages/schedule-service/requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary 2 | flask 3 | jsonpickle 4 | pytz 5 | -------------------------------------------------------------------------------- /packages/utils/atem-control/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.js 3 | *.swp 4 | node_modules/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /tests/data/sine.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frikanalen/frikanalen/HEAD/tests/data/sine.wav -------------------------------------------------------------------------------- /tests/data/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frikanalen/frikanalen/HEAD/tests/data/white.png -------------------------------------------------------------------------------- /tests/data/white-sine.ogv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frikanalen/frikanalen/HEAD/tests/data/white-sine.ogv -------------------------------------------------------------------------------- /packages/frontend/modules/playout/types.ts: -------------------------------------------------------------------------------- 1 | export type MixEffectsBusInput = { index: number; name: string }; 2 | -------------------------------------------------------------------------------- /packages/playout/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | python-dateutil 3 | requests 4 | pytz 5 | python-json-logger 6 | -------------------------------------------------------------------------------- /packages/schedule-service/database/foo.py: -------------------------------------------------------------------------------- 1 | from schedule-service.clock import hello 2 | 3 | clock.hello() 4 | -------------------------------------------------------------------------------- /infra/old/k8s/streaming/000-namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: streaming 5 | -------------------------------------------------------------------------------- /infra/playbooks/requirements.yml: -------------------------------------------------------------------------------- 1 | # Ansible-galaxy requirements 2 | collections: 3 | - name: community.kubernetes 4 | -------------------------------------------------------------------------------- /packages/frontend/modules/form/types/Validator.ts: -------------------------------------------------------------------------------- 1 | export type Validator = (value: T) => Promise 2 | -------------------------------------------------------------------------------- /packages/frontend/modules/lang/async.ts: -------------------------------------------------------------------------------- 1 | export const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); 2 | -------------------------------------------------------------------------------- /packages/utils/atem-control/.dockerignore: -------------------------------------------------------------------------------- 1 | FK_API_URL=http://fk.dev.local/api 2 | FK_APIV2_URL=http://localhost:8080/ -------------------------------------------------------------------------------- /infra/old/k8s/scratchpad.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | helm repo add traefik https://containous.github.io/traefik-helm-chart 5 | 6 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-cloudflare/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | kafka-python 3 | tuspy 4 | requests 5 | smart_open 6 | -------------------------------------------------------------------------------- /infra/old/k8s/README.md: -------------------------------------------------------------------------------- 1 | # k8s 2 | 3 | This directory is being phased out, but used to be the main repository for k8s resources. -------------------------------------------------------------------------------- /infra/playbooks/cluster/tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | remote_user: root 4 | roles: 5 | - cluster 6 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/types.ts: -------------------------------------------------------------------------------- 1 | import * as icons from "./icons"; 2 | 3 | export type IconType = keyof typeof icons; 4 | -------------------------------------------------------------------------------- /packages/frontend/pages/healthz.js: -------------------------------------------------------------------------------- 1 | export const HealthCheck = () =>

All good!

; 2 | 3 | export default HealthCheck; 4 | -------------------------------------------------------------------------------- /infra/old/k8s/multiviewer-streaming/000-namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: multiviewer 5 | -------------------------------------------------------------------------------- /infra/old/roles/playout/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | app_user: fk-playout 3 | app_dir: /srv/fk-playout 4 | domain: frikanalen.no 5 | -------------------------------------------------------------------------------- /packages/frontend/modules/state/helpers/getUniqueId.ts: -------------------------------------------------------------------------------- 1 | let increment = 0; 2 | 3 | export const getUniqueId = () => increment++; 4 | -------------------------------------------------------------------------------- /packages/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frikanalen/frikanalen/HEAD/packages/frontend/public/favicon.ico -------------------------------------------------------------------------------- /packages/utils/prom-check-video-stream/requirements.txt: -------------------------------------------------------------------------------- 1 | prometheus_async 2 | aiohttp 3 | ffmpeg-python 4 | ipython 5 | asyncio 6 | -------------------------------------------------------------------------------- /packages/frontend/modules/lang/types.ts: -------------------------------------------------------------------------------- 1 | export type RequiredKeys = { [K in keyof T]-?: {} extends Pick ? never : K }[keyof T]; 2 | -------------------------------------------------------------------------------- /packages/utils/stills-generator/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frikanalen/frikanalen/HEAD/packages/utils/stills-generator/Roboto-Black.ttf -------------------------------------------------------------------------------- /packages/utils/stills-generator/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frikanalen/frikanalen/HEAD/packages/utils/stills-generator/background.png -------------------------------------------------------------------------------- /infra/playbooks/default-secrets.yml: -------------------------------------------------------------------------------- 1 | postgres_password: defaultpassword1 2 | replication_password: defaultpassword2 3 | fkweb_database_url: connstring goes here 4 | -------------------------------------------------------------------------------- /infra/playbooks/files/rook-ceph-values.yaml: -------------------------------------------------------------------------------- 1 | csi: 2 | kubeletDirPath: "/var/snap/microk8s/common/var/lib/kubelet" 3 | 4 | monitoring: 5 | enabled: true 6 | -------------------------------------------------------------------------------- /packages/playout/cache/dailyplan/plan20200205.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frikanalen/frikanalen/HEAD/packages/playout/cache/dailyplan/plan20200205.pickle -------------------------------------------------------------------------------- /packages/frontend/modules/lang/array.ts: -------------------------------------------------------------------------------- 1 | export const toggleArrayItem = (arr: T[], item: T) => 2 | arr.includes(item) ? arr.filter((i) => i !== item) : [...arr, item]; 3 | -------------------------------------------------------------------------------- /packages/frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | .dockerignore 3 | node_modules 4 | yarn-error.log 5 | .yarnclean 6 | Dockerfile 7 | .git 8 | .gitignore 9 | .npmrc 10 | .next 11 | -------------------------------------------------------------------------------- /infra/old/k8s-dev/config-frontend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: frontend-envs 5 | namespace: default 6 | data: 7 | NEXT_PUBLIC_ENV: "devcluster" 8 | -------------------------------------------------------------------------------- /infra/old/k8s/ingress/service_kiloview.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: kiloview 5 | spec: 6 | type: ExternalName 7 | externalName: 192.168.3.168 8 | -------------------------------------------------------------------------------- /infra/old/k8s-legacy/config-frontend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: frontend-envs 5 | namespace: default 6 | data: 7 | NEXT_PUBLIC_ENV: "production" 8 | -------------------------------------------------------------------------------- /packages/frontend/modules/network/types.ts: -------------------------------------------------------------------------------- 1 | // Represents the REST api collection format 2 | export type ApiCollection = { 3 | count: number; 4 | results: T[]; 5 | next: string | null; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/utils/monitoring-stream-server/deploy.sh: -------------------------------------------------------------------------------- 1 | docker build -t frikanalen/monitoring-stream-ws . && docker push frikanalen/monitoring-stream-ws && kubectl rollout restart deployment monitoring-stream-ws 2 | -------------------------------------------------------------------------------- /packages/frontend/modules/modal/components/PrimaryModal/Body.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const Body = styled.div` 4 | overflow-y: auto; 5 | padding: 24px; 6 | flex: 1; 7 | `; 8 | -------------------------------------------------------------------------------- /infra/old/k8s-legacy/s3-alias.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: s3-backend 5 | spec: 6 | type: ExternalName 7 | externalName: "rook-ceph-rgw-media-store.rook-ceph.svc.cluster.local" 8 | -------------------------------------------------------------------------------- /infra/playbooks/files/prometheus-postgres-exporter.yaml: -------------------------------------------------------------------------------- 1 | serviceMonitor: 2 | enabled: true 3 | 4 | config: 5 | datasource: 6 | datasourceSecret: 7 | name: database-api-secret 8 | key: DATABASE_URL 9 | -------------------------------------------------------------------------------- /packages/utils/monitoring-stream-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16-strictest/tsconfig.json", 3 | "include": [ 4 | "src/**/*.ts" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "dist" 8 | } 9 | } -------------------------------------------------------------------------------- /packages/frontend/modules/state/lists.ts: -------------------------------------------------------------------------------- 1 | import { createVideoList } from "modules/video/lists/createVideoList"; 2 | 3 | export const lists = { 4 | video: createVideoList, 5 | }; 6 | 7 | export type ListType = keyof typeof lists; 8 | -------------------------------------------------------------------------------- /packages/frontend/modules/lang/number.ts: -------------------------------------------------------------------------------- 1 | export const clamp = (v: number, max: number, min = 0) => Math.min(Math.max(v, min), max); 2 | 3 | export function lerp(a: number, b: number, delta: number) { 4 | return a + (b - a) * delta; 5 | } 6 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-cloudflare/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt . 6 | 7 | RUN pip install -r ./requirements.txt 8 | 9 | COPY ./ ./ 10 | 11 | CMD ["./copy-to-legacy"] 12 | -------------------------------------------------------------------------------- /packages/frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /infra/old/roles/debian_stock_config/tasks/hosts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks/hosts.yml 3 | - include_vars: hosts.yml 4 | - name: Generate /etc/hosts file 5 | template: 6 | src=etc/hosts.j2 7 | dest=/etc/hosts 8 | when: "'tx4' in inventory_hostname" 9 | -------------------------------------------------------------------------------- /infra/old/roles/playout/templates/fk-get-schedule-from-backend.timer.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Refresh scheduled video from backend 3 | 4 | [Timer] 5 | OnCalendar=*-*-* 23:00:00 6 | Persistent=true 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /packages/frontend/modules/network/constants.ts: -------------------------------------------------------------------------------- 1 | export const UPLOAD_RETRY_COUNT = 5; 2 | export const TUS_RESUMABLE = "1.0.0"; 3 | export const TUS_CHUNK_SIZE = 10 * 1024 * 1024; // 10mib 4 | export const THE_END_OF_TIMES = "Fri, 31 Dec 9999 23:59:59 GMT"; 5 | -------------------------------------------------------------------------------- /packages/frontend/types/next-env.d.ts: -------------------------------------------------------------------------------- 1 | import { Manager } from "modules/state/types"; 2 | 3 | // Augment Next.js NextPageContext 4 | declare module "next/dist/shared/lib/utils" { 5 | export interface NextPageContext { 6 | manager: Manager; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /infra/old/k8s/oven-media-engine/README: -------------------------------------------------------------------------------- 1 | This streaming server is not in use currently. 2 | 3 | An attempt was made to configure it, but it did not play nice with our outgoing transport stream. 4 | 5 | Perhaps when a new encoder is set up, it will be not broken. 6 | -------------------------------------------------------------------------------- /infra/old/roles/debian_stock_config/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - include: roles/debian_stock_config/tasks/hosts.yml 2 | - include_vars: packages.yml 3 | - name: "Install common Debian base packages" 4 | apt: 5 | name: "{{ base_packages }}" 6 | state: present 7 | -------------------------------------------------------------------------------- /infra/old/roles/playout/templates/fk-get-filler-videos.timer.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Update list of videos that can be ad-hoc scheduled 3 | 4 | [Timer] 5 | OnCalendar=*-*-* 23:00:00 6 | Persistent=true 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /infra/playbooks/external.yaml: -------------------------------------------------------------------------------- 1 | # Monitoring of cluster-external resources 2 | - hosts: localhost 3 | vars: 4 | tasks: 5 | - name: Deploy ZFS monitoring 6 | community.kubernetes.k8s: 7 | state: present 8 | src: files/zfs-monitoring.yaml 9 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Please only submit security vulnerabilities against main head. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Submit an issue if that is acceptable. If it is not, contact toresbe@protonmail.com. 10 | -------------------------------------------------------------------------------- /packages/frontend/modules/modal/contexts.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export type ModalContextValue = { 4 | close: () => void; 5 | dismiss: () => void; 6 | }; 7 | 8 | export const modalContext = createContext(undefined); 9 | -------------------------------------------------------------------------------- /infra/old/k8s/stills-uploader/002-service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: stills-upload 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | app: stills-upload 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | name: web 13 | -------------------------------------------------------------------------------- /packages/frontend/modules/organization/helpers/formatAddress.ts: -------------------------------------------------------------------------------- 1 | import { BrregAddress } from "./fetchBrregData"; 2 | 3 | export const formatAddress = (address: BrregAddress) => { 4 | return address.adresse.join("\n") + "\n" + `${address.postnummer} ${address.poststed}`; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-legacy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-bullseye AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt . 6 | 7 | RUN pip install -r ./requirements.txt 8 | 9 | FROM builder 10 | 11 | COPY copy-to-legacy . 12 | 13 | CMD ["./copy-to-legacy"] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | packages/fkweb/fkbeta/default.db 3 | .idea/ 4 | env 5 | **/.DS_Store 6 | 7 | # vim swap files 8 | [._]*.s[a-v][a-z] 9 | !*.svg # comment out if you don't need vector files 10 | [._]*.sw[a-p] 11 | [._]s[a-rt-v][a-z] 12 | [._]ss[a-gi-z] 13 | [._]sw[a-p] 14 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/styles/linkStyle.ts: -------------------------------------------------------------------------------- 1 | import { css, Theme } from "@emotion/react"; 2 | 3 | export const linkStyle = (props: { theme: Theme }) => css` 4 | color: ${props.theme.color.accent}; 5 | 6 | &:hover { 7 | text-decoration: underline; 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /infra/README: -------------------------------------------------------------------------------- 1 | Infrastructure definitions 2 | 3 | All files here are for reference only and is being replaced with: 4 | 5 | [Frikanalen/infra] - Ansible and everything to build a working k8s cluster 6 | [Frikanalen/gitops] - Everything that gets deployed on that cluster (monitored by ArgoCD) 7 | -------------------------------------------------------------------------------- /infra/old/k8s/monitoring/stream-monitor/002-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: frostguard 5 | spec: 6 | selector: 7 | app: frostguard 8 | ports: 9 | - name: prometheus 10 | protocol: TCP 11 | port: 9001 12 | targetPort: 9001 13 | -------------------------------------------------------------------------------- /infra/old/k8s/stills-generator/002-service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: stills-generator 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | app: stills-generator 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | name: web 13 | -------------------------------------------------------------------------------- /packages/frontend/pages/organization/[organizationId]/plan.module.sass: -------------------------------------------------------------------------------- 1 | .scheduleItem 2 | margin-top: 15px 3 | padding: 5px 4 | background: rgba(255,255,255,0.2) 5 | 6 | .startTime 7 | flex-grow: 0 8 | 9 | .endTime 10 | flex-grow: 0 11 | 12 | .videoTitle 13 | flex-grow: 1 -------------------------------------------------------------------------------- /packages/frontend/modules/modal/components/PrimaryModal/Footer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const Footer = styled.div` 4 | padding: 24px; 5 | padding-top: 0px; 6 | 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | `; 11 | -------------------------------------------------------------------------------- /packages/frontend/modules/popover/components/PrimaryPopover.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const PrimaryPopover = styled.div` 4 | background: ${(props) => props.theme.color.card}; 5 | box-shadow: 2px 2px 11px 2px rgba(0, 0, 0, 0.1); 6 | 7 | border-radius: 4px; 8 | `; 9 | -------------------------------------------------------------------------------- /infra/old/k8s/streaming/003-service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: shaka-packager 5 | namespace: streaming 6 | spec: 7 | type: ClusterIP 8 | selector: 9 | app: shaka-packager 10 | ports: 11 | - protocol: TCP 12 | port: 80 13 | name: web 14 | -------------------------------------------------------------------------------- /infra/playbooks/files/elastic-values.yaml: -------------------------------------------------------------------------------- 1 | eck-kibana: 2 | enabled: true 3 | ingress: 4 | host: kibana.admin.frikanalen.no 5 | annotations: 6 | eck.k8s.elastic.co/license: basic 7 | 8 | eck-elasticsearch: 9 | enabled: true 10 | annotations: 11 | eck.k8s.elastic.co/license: basic 12 | -------------------------------------------------------------------------------- /infra/old/k8s/multiviewer-streaming/003-service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: shaka-packager 5 | namespace: streaming 6 | spec: 7 | type: ClusterIP 8 | selector: 9 | app: shaka-packager 10 | ports: 11 | - protocol: TCP 12 | port: 80 13 | name: web 14 | -------------------------------------------------------------------------------- /infra/old/roles/playout/templates/fk-get-schedule-from-backend.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | AssertPathExists={{app_dir}}/app 3 | 4 | [Service] 5 | Type=oneshot 6 | User={{app_user}} 7 | Group={{app_user}} 8 | WorkingDirectory={{app_dir}}/app 9 | ExecStart={{app_dir}}/env/bin/python fk-get-schedule-from-backend 2 10 | -------------------------------------------------------------------------------- /packages/frontend/modules/popover/types/Popover.ts: -------------------------------------------------------------------------------- 1 | import { Placement } from "@popperjs/core" 2 | 3 | export type Popover = { 4 | name: string 5 | placement: Placement 6 | anchor: HTMLElement 7 | render: () => React.ReactNode 8 | onDismiss?: () => void 9 | autoDismiss?: boolean 10 | } 11 | -------------------------------------------------------------------------------- /packages/frontend/modules/popover/contexts.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Popover } from "./types/Popover"; 3 | 4 | export type PopoverContext = { 5 | dismiss: () => void; 6 | popover: Popover; 7 | }; 8 | 9 | export const popoverContext = React.createContext(undefined); 10 | -------------------------------------------------------------------------------- /packages/utils/atem-control/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18-strictest-esm/tsconfig.json", 3 | "include": [ 4 | "src/**/*.ts" 5 | ], 6 | "compilerOptions": { 7 | "module": "ESNext", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "esModuleInterop": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/utils/obe-service/fk-obe.conf: -------------------------------------------------------------------------------- 1 | VIDEO_OPTS=vbv-maxrate=7600,vbv-bufsize=7600,bitrate=7600,keyint=24,threads=4,aspect-ratio=16:9,pid=564 2 | AUDIO_OPTS=bitrate=192,format=aac,aac-profile=he-aac-v1,aac-encap=latm,lang=nor,pid=768 3 | MUX_OPTS=ts-muxrate=8000000,ts-type=dvb,ts-id=311,program-num=311,pmt-pid=311,pcr-pid=564,cbr=1 4 | -------------------------------------------------------------------------------- /infra/old/hosts: -------------------------------------------------------------------------------- 1 | [encoder] 2 | obe-hd.frikanalen.no 3 | 4 | [tx] 5 | tx1.frikanalen.no 6 | tx2.frikanalen.no 7 | tx3.frikanalen.no 8 | tx4.frikanalen.no 9 | 10 | [upload] 11 | file01.frikanalen.no nginx=true domain=upload.frikanalen.no 12 | 13 | [playout] 14 | tx2.frikanalen.no 15 | 16 | [k8s_master] 17 | simula.frikanalen.no 18 | -------------------------------------------------------------------------------- /packages/utils/obe-service/fk-obe.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Frikanalen OBE encoder 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=fk-obe 8 | ExecStart=/usr/local/bin/fk-obe 9 | EnvironmentFile=/etc/fk-obe.conf 10 | Restart=on-abort 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | 15 | -------------------------------------------------------------------------------- /infra/old/charts/frontend/templates/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Service 3 | apiVersion: v1 4 | metadata: 5 | name: {{ .Release.Name }}-frontend 6 | namespace: {{ .Values.namespace }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | selector: 10 | app: frontend 11 | ports: 12 | - protocol: TCP 13 | port: 3000 14 | name: web 15 | -------------------------------------------------------------------------------- /infra/old/k8s/debug/001-dnsutils.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: dnsutils 5 | namespace: default 6 | spec: 7 | containers: 8 | - name: dnsutils 9 | image: gcr.io/kubernetes-e2e-test-images/dnsutils:1.3 10 | command: 11 | - sleep 12 | - "3600" 13 | imagePullPolicy: IfNotPresent 14 | restartPolicy: Always 15 | -------------------------------------------------------------------------------- /packages/frontend/modules/modal/hooks/useModal.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { modalContext } from "../contexts"; 3 | 4 | export const useModal = () => { 5 | const context = useContext(modalContext); 6 | 7 | if (!context) { 8 | throw new Error("useModal() called outside modal context"); 9 | } 10 | 11 | return context; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/utils/atem-control/src/poster/router.ts: -------------------------------------------------------------------------------- 1 | import { posterPreview, posterUpload } from "./handlers.js"; 2 | import { checkStaff } from "../auth/checkStaff.js"; 3 | import express from "express"; 4 | 5 | export const posterRouter = express.Router(); 6 | 7 | posterRouter.get("/preview", posterPreview); 8 | posterRouter.post("/upload", [checkStaff, posterUpload]); 9 | -------------------------------------------------------------------------------- /packages/utils/atem-control/src/auth/checkStaff.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "express"; 2 | import { checkIfStaff } from "../auth.js"; 3 | 4 | export const checkStaff: RequestHandler = async (req, res, next) => { 5 | if (!(await checkIfStaff(req.cookies))) { 6 | res.status(403).send("User must be staff"); 7 | return; 8 | } 9 | next(); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/schedule-service/README.md: -------------------------------------------------------------------------------- 1 | These python files retrieve schedule data from the backend database and present it in forms suitable for the playout. 2 | 3 | It is currently used by the playout, and might be used to generate EPG data for distributors soon. 4 | 5 | This is a bit of a hack. I'm not quite sure what this will become. It might be best to merge this into fkweb somehow. 6 | -------------------------------------------------------------------------------- /packages/frontend/modules/modal/components/PrimaryModal/index.ts: -------------------------------------------------------------------------------- 1 | import { Actions } from "./Actions"; 2 | import { Body } from "./Body"; 3 | import { Container } from "./Container"; 4 | import { Footer } from "./Footer"; 5 | import { Header } from "./Header"; 6 | 7 | export const PrimaryModal = { 8 | Container, 9 | Actions, 10 | Footer, 11 | Header, 12 | Body, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/frontend/modules/state/hooks/useObserver.ts: -------------------------------------------------------------------------------- 1 | import { autorun } from "mobx"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export const useObserver = (fn: () => T) => { 5 | const [value, setValue] = useState(fn()); 6 | 7 | useEffect(() => { 8 | return autorun(() => { 9 | setValue(fn()); 10 | }); 11 | }, []); 12 | 13 | return value; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/utils/monitoring-stream-server/README.md: -------------------------------------------------------------------------------- 1 | This utility receives an HTTP stream from ffmpeg running as a bare-metal systemd service on tx1, which is fed by an AUX on the production switcher. 2 | 3 | Systemd file is in this directory for safe keeping. 4 | 5 | It then serves it via websockets using jsmpeg, which is really neat because it saves us having to deal with the monster that is WebRTC. 6 | -------------------------------------------------------------------------------- /packages/utils/upload-receiver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tusproject/tusd 2 | 3 | USER root 4 | COPY requirements.txt . 5 | 6 | RUN apk add py3-pip py3-paramiko py3-requests 7 | 8 | USER tusd 9 | 10 | COPY hooks /srv/tusd-hooks 11 | CMD ["-behind-proxy","--hooks-enabled-events","pre-create,pre-finish","-upload-dir","./upload-tmp","-hooks-dir","/srv/tusd-hooks","-base-path","/api/videos/upload"] 12 | -------------------------------------------------------------------------------- /packages/utils/atem-control/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN yarn install --quiet --dev 8 | RUN yarn build 9 | 10 | FROM node:18-alpine 11 | 12 | COPY --from=builder /app/dist dist 13 | COPY --from=builder /app/node_modules node_modules 14 | COPY yarn.lock package.json tsconfig.json ./ 15 | 16 | EXPOSE 80 17 | 18 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /packages/frontend/modules/playout/constants.ts: -------------------------------------------------------------------------------- 1 | import { MixEffectsBusInput } from "./types"; 2 | 3 | export const ATEM_INPUTS: MixEffectsBusInput[] = [ 4 | { index: 1, name: "TX1" }, 5 | { index: 2, name: "TX2" }, 6 | { index: 3, name: "TX3" }, 7 | { index: 5, name: "RX1" }, 8 | { index: 7, name: "DEV" }, 9 | { index: 3010, name: "Still 1" }, 10 | { index: 1000, name: "Color bars" }, 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/frontend/modules/popover/hooks/usePopoverContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { popoverContext } from "../contexts"; 3 | 4 | export const usePopoverContext = () => { 5 | const context = useContext(popoverContext); 6 | 7 | if (!context) { 8 | throw new Error("usePopoverContext() called outside popover context"); 9 | } 10 | 11 | return context; 12 | }; 13 | -------------------------------------------------------------------------------- /infra/old/k8s-dev/dummy_atem.yaml: -------------------------------------------------------------------------------- 1 | # This file defines a "loopback" ingress point, to a local instance of atem-control. 2 | # Replace the IP address on the last line with your workstation's IP, and the 3 | # requests will be routed through the ingress to your box. 4 | kind: Service 5 | apiVersion: v1 6 | metadata: 7 | name: atem-control 8 | spec: 9 | type: ExternalName 10 | externalName: 192.168.135.111 11 | -------------------------------------------------------------------------------- /infra/old/k8s-dev/dummy_frontend.yaml: -------------------------------------------------------------------------------- 1 | # This file defines a "loopback" ingress point, to a local instance of atem-control. 2 | # Replace the IP address on the last line with your workstation's IP, and the 3 | # requests will be routed through the ingress to your box. 4 | kind: Service 5 | apiVersion: v1 6 | metadata: 7 | name: frontend 8 | spec: 9 | type: ExternalName 10 | externalName: 192.168.135.235 11 | -------------------------------------------------------------------------------- /packages/frontend/modules/core/constants.ts: -------------------------------------------------------------------------------- 1 | export const CONTENT_WIDTH = 1500; 2 | export const CONTENT_WIDTH_PADDING = 24 * 2; 3 | 4 | export const MOBILE_MENU_THRESHOLD = 600; 5 | 6 | export const IS_SERVER = typeof window === "undefined"; 7 | export const ARTIFICIAL_DELAY = IS_SERVER ? 0 : 500; 8 | 9 | export const CANONICAL_HOST = "https://frikanalen.no"; 10 | export const WEBSITE_NAME = "Frikanalen"; 11 | -------------------------------------------------------------------------------- /infra/old/charts/frontend/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "frontend.serviceAccountName" . }} 6 | labels: 7 | {{- include "frontend.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/Document.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const Document = styled.div` 4 | h2 { 5 | margin: 24px 0px; 6 | } 7 | 8 | h3 { 9 | margin: 12px 0px; 10 | } 11 | 12 | ul, 13 | ol { 14 | padding-left: 16px; 15 | } 16 | 17 | li { 18 | margin: 8px 0px; 19 | } 20 | 21 | ul > li { 22 | list-style-type: disc; 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /infra/old/k8s-dev/dummy_upload_receiver.yaml: -------------------------------------------------------------------------------- 1 | # This file defines a "loopback" ingress point, to a local instance of atem-control. 2 | # Replace the IP address on the last line with your workstation's IP, and the 3 | # requests will be routed through the ingress to your box. 4 | kind: Service 5 | apiVersion: v1 6 | metadata: 7 | name: upload-receiver 8 | spec: 9 | type: ExternalName 10 | externalName: 192.168.135.111 11 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/hooks/useWindowEvent.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export const useWindowEvent = ( 4 | name: T, 5 | callback: (event: WindowEventMap[T]) => void 6 | ) => { 7 | useEffect(() => { 8 | window.addEventListener(name, callback); 9 | 10 | return () => { 11 | window.removeEventListener(name, callback); 12 | }; 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/utils/atem-control/src/atem/router.ts: -------------------------------------------------------------------------------- 1 | import { getProgram, getPreview, setProgram, setPreview } from "./handlers.js"; 2 | import { checkStaff } from "../auth/checkStaff.js"; 3 | import express from "express"; 4 | 5 | export const atemRouter = express.Router(); 6 | 7 | atemRouter.route("/program").get(getProgram).post([checkStaff, setProgram]); 8 | atemRouter.route("/preview").get(getPreview).post([checkStaff, setPreview]); 9 | -------------------------------------------------------------------------------- /infra/old/k8s/ingress/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: traefik 5 | namespace: default 6 | labels: 7 | release: prometheus-stack 8 | spec: 9 | endpoints: 10 | - path: /metrics 11 | port: admin 12 | scheme: http 13 | namespaceSelector: 14 | matchNames: 15 | - default 16 | selector: 17 | matchLabels: 18 | app: traefik 19 | -------------------------------------------------------------------------------- /packages/frontend/modules/core/components/NavLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HeaderLink } from "./HeaderLink"; 3 | 4 | export function NavLinks() { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/frontend/modules/form/hooks/useForm.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import { ObservableForm } from "../classes/ObservableForm" 3 | import { formContext } from "../components/Form" 4 | 5 | export const useForm = >() => { 6 | const form = useContext(formContext) 7 | 8 | if (!form) { 9 | throw new Error("Form not found in context") 10 | } 11 | 12 | return form as F 13 | } 14 | -------------------------------------------------------------------------------- /packages/schedule-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine as base 2 | 3 | WORKDIR /srv/frikanalen 4 | 5 | RUN apk update && apk add postgresql-dev gcc python3-dev musl-dev 6 | 7 | FROM base AS dependencies 8 | 9 | COPY requirements.txt ./ 10 | 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | FROM dependencies 14 | 15 | COPY . . 16 | 17 | CMD ["flask", "run", "-h", "0.0.0.0", "-p", "80"] 18 | 19 | EXPOSE 80 20 | -------------------------------------------------------------------------------- /infra/old/k8s/helm-values/prometheus-postgres-exporter-beta.yaml: -------------------------------------------------------------------------------- 1 | serviceMonitor: 2 | enabled: true 3 | namespace: monitoring 4 | labels: 5 | release: prometheus-stack 6 | 7 | prometheusRule: 8 | enabled: false 9 | 10 | config: 11 | datasource: 12 | host: postgres 13 | user: postgres 14 | database: fk 15 | passwordSecret: 16 | name: database 17 | key: POSTGRES_PASSWORD 18 | sslmode: disable 19 | -------------------------------------------------------------------------------- /packages/frontend/modules/core/styles/mainContentStyle.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/react"; 2 | import { CONTENT_WIDTH, CONTENT_WIDTH_PADDING } from "../constants"; 3 | 4 | export const mainContentStyle = css` 5 | max-width: ${CONTENT_WIDTH}px; 6 | width: 100%; 7 | 8 | @media (max-width: ${CONTENT_WIDTH + CONTENT_WIDTH_PADDING}px) { 9 | max-width: 100%; 10 | width: 100%; 11 | 12 | padding: 0px 24px; 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /packages/frontend/modules/user/schemas.ts: -------------------------------------------------------------------------------- 1 | export type OrganizationRole = { 2 | role: string; 3 | organizationId: number; 4 | organizationName: string; 5 | }; 6 | 7 | export type User = { 8 | id: number; 9 | email: string; 10 | firstName: string; 11 | lastName: string; 12 | phoneNumber: string; 13 | isStaff: string; 14 | dateJoined: string; 15 | dateOfBirth: string; 16 | organizationRoles: OrganizationRole[]; 17 | }; 18 | -------------------------------------------------------------------------------- /infra/old/k8s/ingest/upload-processor-legacy/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: upload-processors 5 | namespace: default 6 | data: 7 | s3_endpoint_url: | 8 | http://192.168.3.36:8085 9 | ssh_public_key: | 10 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFo77IG5gzHH9T9/3/B8oqimEoXn6iVWbRqokar3/HkToe7/mGvJUDQrUtnOD6k5ZLXCrFNNRCDzquOEynVgOe4= fkupload@file01 ssh key 11 | -------------------------------------------------------------------------------- /infra/old/roles/upload-app/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | repo_url: https://github.com/frikanalen/frikanalen.git 3 | app_dir: /srv/fkupload 4 | app_user: fkupload 5 | app_bind: 0.0.0.0:3000 6 | fk_auth_token: test 7 | process_app_dir: "{{app_dir}}/process" 8 | upload_app_dir: "{{app_dir}}/upload" 9 | upload_dir: "{{app_dir}}/upload_files" 10 | upload_finished_dir: "{{app_dir}}/finished_uploads" 11 | fk_api_url: "https://forrige.frikanalen.no/api" 12 | -------------------------------------------------------------------------------- /packages/frontend/modules/form/helpers/checkIfFieldIsReady.ts: -------------------------------------------------------------------------------- 1 | import { Field } from "../classes/ObservableForm" 2 | 3 | export type Readyable = Field & { 4 | ready: boolean 5 | } 6 | 7 | const isReadyable = (field: Field): field is Readyable => { 8 | return "ready" in field 9 | } 10 | 11 | export const checkIfFieldIsReady = (field: Field) => { 12 | if (isReadyable(field)) { 13 | return field.ready 14 | } 15 | 16 | return true 17 | } 18 | -------------------------------------------------------------------------------- /packages/frontend/modules/modal/components/PrimaryModal/Actions.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { ButtonList } from "modules/ui/components/ButtonList"; 3 | import { PropsWithChildren } from "react"; 4 | 5 | export const Container = styled(ButtonList)` 6 | justify-content: flex-end; 7 | `; 8 | 9 | export function Actions(props: PropsWithChildren<{}>) { 10 | return {props.children}; 11 | } 12 | -------------------------------------------------------------------------------- /infra/old/k8s/ingress/ingress_kiloview.yaml: -------------------------------------------------------------------------------- 1 | kind: IngressRoute 2 | apiVersion: traefik.containo.us/v1alpha1 3 | metadata: 4 | name: kiloview 5 | namespace: default 6 | spec: 7 | entryPoints: 8 | - websecure 9 | routes: 10 | - match: Host(`kiloview.frikanalen.no`) 11 | kind: Rule 12 | services: 13 | - kind: Service 14 | name: kiloview 15 | scheme: http 16 | port: 80 17 | tls: 18 | certResolver: default 19 | -------------------------------------------------------------------------------- /infra/old/roles/playout/templates/fk-playout.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | AssertPathExists={{app_dir}}/app 3 | 4 | [Unit] 5 | Description=Frikanalen Playout 6 | After=syslog.target network.target mnt-media.mount 7 | 8 | [Service] 9 | Type=simple 10 | User={{app_user}} 11 | Group={{app_user}} 12 | WorkingDirectory={{app_dir}}/app 13 | ExecStart={{app_dir}}/env/bin/python fk-playout-service 14 | Restart=always 15 | 16 | [Install] 17 | WantedBy=default.target 18 | -------------------------------------------------------------------------------- /packages/frontend/modules/schedule/helpers/humanizeSelectedScheduleDate.ts: -------------------------------------------------------------------------------- 1 | import { format, isToday, isTomorrow, isYesterday } from "date-fns"; 2 | import { nb } from "date-fns/locale"; 3 | 4 | export const humanizeSelectedScheduleDate = (date: Date) => { 5 | if (isToday(date)) return "i dag"; 6 | if (isTomorrow(date)) return "i morgen"; 7 | if (isYesterday(date)) return "i går"; 8 | 9 | return format(date, "d MMMM", { locale: nb }); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/frontend/modules/state/types.ts: -------------------------------------------------------------------------------- 1 | import { Stores } from "./stores"; 2 | import { StoreManager } from "./classes/StoreManager"; 3 | import { List } from "./classes/List"; 4 | 5 | export type Manager = StoreManager; 6 | 7 | export type StoreFactories = { 8 | [K in keyof Stores]: (manager: Manager) => Stores[K]; 9 | }; 10 | 11 | export type ListFactory = (data: D, manager: Manager) => List; 12 | -------------------------------------------------------------------------------- /packages/utils/monitoring-stream-server/Dockerfile: -------------------------------------------------------------------------------- 1 | from node:16-alpine as builder 2 | 3 | workdir /app 4 | 5 | copy . . 6 | 7 | RUN "yarn" 8 | RUN ["yarn", "build"] 9 | 10 | from node:16-alpine 11 | 12 | workdir /app 13 | 14 | copy package.json yarn.lock ./ 15 | 16 | copy --from=builder /app/dist dist 17 | copy --from=builder /app/node_modules node_modules 18 | 19 | EXPOSE 8081 20 | EXPOSE 8082 21 | 22 | CMD ["yarn", "run", "start", "monitoring"] 23 | -------------------------------------------------------------------------------- /infra/old/k8s/rook-ceph/dashboard-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: rook-ceph-mgr-dash 5 | namespace: rook-ceph 6 | labels: 7 | app: rook-ceph-mgr 8 | rook_cluster: rook-ceph 9 | spec: 10 | ports: 11 | - name: dashboard 12 | port: 8083 13 | protocol: TCP 14 | targetPort: 8443 15 | selector: 16 | app: rook-ceph-mgr 17 | rook_cluster: rook-ceph 18 | sessionAffinity: None 19 | type: NodePort 20 | -------------------------------------------------------------------------------- /infra/old/k8s-legacy/ingress-frontend.yaml: -------------------------------------------------------------------------------- 1 | kind: IngressRoute 2 | apiVersion: traefik.containo.us/v1alpha1 3 | metadata: 4 | name: frontend 5 | namespace: default 6 | spec: 7 | entryPoints: 8 | - websecure 9 | routes: 10 | - match: Host(`www.frikanalen.no`) || Host(`frikanalen.no`) 11 | kind: Rule 12 | services: 13 | - kind: Service 14 | name: frontend 15 | scheme: http 16 | port: 3000 17 | tls: 18 | certResolver: default 19 | -------------------------------------------------------------------------------- /infra/playbooks/HLS_PGM.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | vars: 3 | namespace: beta 4 | domain: beta.frikanalen.no 5 | tasks: 6 | - name: Create namespace 7 | k8s: 8 | name: "{{ namespace }}" 9 | api_version: v1 10 | kind: Namespace 11 | state: present 12 | 13 | - name: Set up live packager 14 | community.kubernetes.k8s: 15 | state: present 16 | definition: "{{lookup ('template', 'stream-hls-pgm.yaml')}}" 17 | -------------------------------------------------------------------------------- /infra/old/charts/frontend/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /packages/frontend/modules/form/hooks/useField.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import { fieldsContext } from "../components/FieldsProvider" 3 | import { Field } from "../classes/ObservableForm" 4 | 5 | export const useField = (name: string) => { 6 | const fields = useContext(fieldsContext) 7 | 8 | if (!fields) { 9 | throw new Error("Fields not found in context") 10 | } 11 | 12 | const field = fields[name] as F 13 | return field 14 | } 15 | -------------------------------------------------------------------------------- /packages/frontend/types/jsmpeg-player.d.ts: -------------------------------------------------------------------------------- 1 | export as namespace JSMpeg; 2 | 3 | declare module "@cycjimmy/jsmpeg-player" { 4 | export type JSMpegOptions = { audioBufferSize: number; videoBufferSize: number }; 5 | export class Player { 6 | setVolume(volume: number); 7 | getVolume(): number; 8 | } 9 | export class VideoElement { 10 | constructor(element: HTMLElement, uri: string, options: JSMpegOptions); 11 | destroy(); 12 | player: Player; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/utils/frostguard/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jrottenberg/ffmpeg:snapshot-alpine AS builder 2 | 3 | RUN apk add python3 build-base python3-dev py3-aiohttp curl 4 | 5 | ENV VIRTUAL_ENV=/opt/venv 6 | RUN python3 -m venv $VIRTUAL_ENV 7 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 8 | 9 | # Install dependencies: 10 | COPY requirements.txt . 11 | RUN pip install -r requirements.txt 12 | 13 | FROM builder 14 | 15 | COPY monitor.py . 16 | 17 | ENTRYPOINT './monitor.py' 18 | CMD ['./monitor.py'] 19 | -------------------------------------------------------------------------------- /packages/utils/stills-generator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | 3 | MAINTAINER Tore Sinding Bekkedal 4 | 5 | RUN mkdir -p /usr/src/app 6 | WORKDIR /usr/src/app 7 | 8 | COPY requirements.txt /usr/src/app/ 9 | 10 | RUN apk add py3-pillow && pip install --no-cache-dir -r requirements.txt 11 | ENV PYTHONPATH /usr/lib/python3.8/site-packages 12 | 13 | COPY . /usr/src/app 14 | 15 | EXPOSE 80 16 | 17 | CMD [ "gunicorn", "-b", "0.0.0.0:80", "app:app" ] 18 | -------------------------------------------------------------------------------- /infra/argocd/django-backend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: fkweb 5 | spec: 6 | project: default 7 | source: 8 | repoURL: https://github.com/Frikanalen/frikanalen 9 | targetRevision: argocd 10 | path: packages/fkweb 11 | helm: 12 | valueFiles: 13 | - values.yaml 14 | destination: 15 | server: https://kubernetes.default.svc 16 | namespace: default 17 | syncPolicy: 18 | automated: {} 19 | 20 | -------------------------------------------------------------------------------- /infra/old/k8s/atem-control/002-service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: atem-control 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | app: atem-control 9 | ports: 10 | # Three types of ports for a service 11 | # nodePort - a static port assigned on each the node 12 | # port - port exposed internally in the cluster 13 | # targetPort - the container port to send requests to 14 | - protocol: TCP 15 | port: 80 16 | name: web 17 | -------------------------------------------------------------------------------- /infra/old/k8s/ingress/old/005-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: IngressRoute 3 | metadata: 4 | name: traefik-dashboard 5 | spec: 6 | entryPoints: 7 | - websecure 8 | routes: 9 | - match: Host(`traefik.admin.frikanalen.no`) 10 | kind: Rule 11 | middlewares: 12 | - name: basic-admin-auth 13 | services: 14 | - name: api@internal 15 | kind: TraefikService 16 | port: 8100 17 | tls: 18 | certResolver: default 19 | -------------------------------------------------------------------------------- /infra/old/roles/playout/templates/mnt-media.mount.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Frikanalen media mount 3 | Wants=networking.service remote-fs.target network-online.target systemd-resolved.service 4 | After=networking.service remote-fs.target network-online.target systemd-resolved.service 5 | 6 | [Mount] 7 | What=//192.168.3.59/media 8 | Where=/mnt/media 9 | Options=_netdev,credentials=/etc/smb_creds,ro,ip=192.168.3.59 10 | Type=cifs 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /infra/old/charts/frontend/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "frontend.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "frontend.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "frontend.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /infra/old/k8s/helm-values/prometheus-postgres-exporter.yaml: -------------------------------------------------------------------------------- 1 | serviceMonitor: 2 | enabled: true 3 | namespace: monitoring 4 | labels: 5 | release: prometheus-stack 6 | 7 | prometheusRule: 8 | enabled: false 9 | 10 | config: 11 | datasource: 12 | # Specify one of both datasource or datasourceSecret 13 | host: database-api 14 | user: postgres 15 | database: fkweb 16 | passwordSecret: 17 | name: database-api-secret 18 | key: POSTGRES_PASSWORD 19 | sslmode: disable 20 | -------------------------------------------------------------------------------- /infra/playbooks/templates/database/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: prometheus-postgres-exporter 5 | namespace: "monitoring" 6 | labels: 7 | release: prometheus-stack 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: prometheus-postgres-exporter 12 | namespaceSelector: 13 | matchNames: 14 | - "{{ namespace }}" 15 | endpoints: 16 | - port: exporter 17 | interval: 30s 18 | path: /metrics 19 | -------------------------------------------------------------------------------- /infra/old/k8s/monitoring/grafana-ingress.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: IngressRoute 3 | apiVersion: traefik.containo.us/v1alpha1 4 | metadata: 5 | name: grafana 6 | namespace: default 7 | spec: 8 | entryPoints: 9 | - websecure 10 | routes: 11 | - match: Host(`grafana.admin.frikanalen.no`) 12 | kind: Rule 13 | services: 14 | - kind: Service 15 | name: prometheus-stack-grafana 16 | namespace: monitoring 17 | scheme: http 18 | port: 80 19 | tls: 20 | certResolver: default 21 | -------------------------------------------------------------------------------- /infra/old/k8s/stills-uploader/001-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: stills-upload 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: stills-upload 9 | minReadySeconds: 5 10 | template: 11 | metadata: 12 | labels: 13 | app: stills-upload 14 | spec: 15 | containers: 16 | - name: stills-upload 17 | image: frikanalen/atem-stills-upload:latest 18 | ports: 19 | - name: web 20 | containerPort: 80 21 | -------------------------------------------------------------------------------- /infra/old/roles/debian_stock_config/templates/etc/hosts.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 3 | ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 4 | 5 | {% for item, IP in static_hosts.items() %} 6 | {% set short_name = item.split('.') %} 7 | {{ IP }} {{ item }} {{ short_name[0] }} 8 | {% endfor %} 9 | 10 | # The following lines are desirable for IPv6 capable hosts 11 | ff02::1 ip6-allnodes 12 | ff02::2 ip6-allrouters 13 | -------------------------------------------------------------------------------- /packages/playout/Dockerfile: -------------------------------------------------------------------------------- 1 | # This used to be a staged build, but it was more trouble than it was worth, 2 | # with builds missing requirements seemingly at random. Since this is such 3 | # a mission-critical component, and updates are relatively infrequent, I've 4 | # decided to incur the penalty of building from scratch every time. 5 | FROM python:3-buster 6 | 7 | COPY requirements.txt . 8 | 9 | RUN pip install -r ./requirements.txt 10 | 11 | # copy in the rest of the app 12 | COPY ./ ./ 13 | 14 | CMD ["./playout"] 15 | -------------------------------------------------------------------------------- /packages/frontend/modules/playout/forms/createTextSlideForm.ts: -------------------------------------------------------------------------------- 1 | import { ObservableForm } from "modules/form/classes/ObservableForm"; 2 | import { string } from "modules/form/fields/string"; 3 | import { Manager } from "modules/state/types"; 4 | 5 | export const createTextSlideForm = (manager: Manager) => { 6 | return new ObservableForm( 7 | { 8 | heading: string(), 9 | text: string(), 10 | }, 11 | manager 12 | ); 13 | }; 14 | 15 | export type TextSlideForm = ReturnType; 16 | -------------------------------------------------------------------------------- /packages/utils/monitoring-stream-server/fk-multiviewer-stream.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Multiviewer encoder for Janus WebRTC Server 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | ExecStart=/opt/ffmpeg/bin/ffmpeg -f decklink -rtbufsize 100M -i "DeckLink SDI 4K" -codec:v mpeg1video -s 720x576 -aspect 16:9 -b:v 0 -b:v 1.5M -r 50 -ac 1 -ar 44100 -f mpegts -muxdelay 0.001 -b 0 http://192.168.3.100:8081/monitoring 8 | Restart=on-abnormal 9 | LimitNOFILE=65536 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /infra/old/k8s/stills-generator/001-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: stills-generator 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: stills-generator 9 | minReadySeconds: 5 10 | template: 11 | metadata: 12 | labels: 13 | app: stills-generator 14 | spec: 15 | containers: 16 | - name: stills-generator 17 | image: frikanalen/stills-generator:latest 18 | ports: 19 | - name: web 20 | containerPort: 80 21 | -------------------------------------------------------------------------------- /infra/old/k8s-legacy/ingress-upload-receiver.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: IngressRoute 3 | apiVersion: traefik.containo.us/v1alpha1 4 | metadata: 5 | name: upload-receiver 6 | namespace: default 7 | spec: 8 | entryPoints: 9 | - websecure 10 | routes: 11 | - match: (Host(`frikanalen.no`) || Host(`www.frikanalen.no`)) && (PathPrefix(`/api/videos/upload`)) 12 | kind: Rule 13 | services: 14 | - kind: Service 15 | name: upload-receiver 16 | scheme: http 17 | port: 1080 18 | tls: 19 | certResolver: default 20 | -------------------------------------------------------------------------------- /infra/old/k8s/rook-ceph/service-monitor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: ServiceMonitor 3 | metadata: 4 | name: rook-ceph-mgr 5 | namespace: rook-ceph 6 | labels: 7 | team: rook 8 | release: prometheus-stack 9 | spec: 10 | namespaceSelector: 11 | matchNames: 12 | - rook-ceph 13 | selector: 14 | matchLabels: 15 | app: rook-ceph-mgr 16 | rook_cluster: rook-ceph 17 | ceph_daemon_id: a 18 | endpoints: 19 | - port: http-metrics 20 | path: /metrics 21 | interval: 5s 22 | -------------------------------------------------------------------------------- /infra/old/roles/upload-app/templates/move_and_process.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | AssertPathExists={{process_app_dir}} 3 | 4 | [Service] 5 | User={{app_user}} 6 | Group={{app_user}} 7 | WorkingDirectory={{process_app_dir}} 8 | Environment=FK_API={{fk_api_url}} 9 | Environment=FK_TOKEN={{fk_auth_token}} 10 | ExecStart={{app_dir}}/env/bin/python3 move_and_process.py daemon --indir "{{upload_finished_dir}}" --outdir "{{fk_media_dir}}" 11 | Restart=always 12 | PrivateTmp=true 13 | #NoNewPrivileges=true 14 | 15 | [Install] 16 | WantedBy=default.target 17 | -------------------------------------------------------------------------------- /infra/playbooks/templates/beta/media/ingress.yaml: -------------------------------------------------------------------------------- 1 | ## 2 | # For end users 3 | ## 4 | kind: IngressRoute 5 | apiVersion: traefik.containo.us/v1alpha1 6 | metadata: 7 | name: media-ingress 8 | namespace: "{{ namespace }}" 9 | spec: 10 | tls: 11 | certResolver: default 12 | entryPoints: 13 | - websecure 14 | routes: 15 | - match: Host(`{{ domain }}`) && PathPrefix(`/media`) 16 | kind: Rule 17 | services: 18 | - kind: Service 19 | name: rook-ceph-rgw-media-store 20 | namespace: rook-ceph 21 | scheme: http 22 | port: 80 23 | -------------------------------------------------------------------------------- /packages/frontend/modules/state/hooks/useCookie.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useStores } from "../manager"; 3 | import { CookieType } from "../stores/cookieStore"; 4 | 5 | export const useCookie = (key: CookieType, initial: T) => { 6 | const { cookieStore } = useStores(); 7 | const [state, setState] = useState(() => (cookieStore.get(key) as T) ?? initial); 8 | 9 | useEffect(() => { 10 | cookieStore.set(key, state); 11 | }, [state, key, cookieStore]); 12 | 13 | return [state, setState] as const; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/frontend/modules/state/hooks/useResourceList.ts: -------------------------------------------------------------------------------- 1 | import { List } from "../classes/List"; 2 | import { Resource } from "../classes/Resource"; 3 | import { useObserver } from "./useObserver"; 4 | 5 | // TODO: Fix this, it used useObserver in the past which has been deprecated 6 | export const useResourceList = >( 7 | list: List, 8 | store: { 9 | getResourceById: (id: number) => R; 10 | } 11 | ) => { 12 | const resources = useObserver(() => list.items.map((id) => store.getResourceById(id))); 13 | return resources; 14 | }; 15 | -------------------------------------------------------------------------------- /infra/old/k8s/monitoring/stream-monitor/001-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: frostguard 5 | labels: 6 | app: frostguard 7 | spec: 8 | replicas: 1 9 | serviceName: frostguard 10 | selector: 11 | matchLabels: 12 | app: frostguard 13 | template: 14 | metadata: 15 | labels: 16 | app: frostguard 17 | spec: 18 | containers: 19 | - name: frostguard 20 | image: frikanalen/frostguard 21 | imagePullPolicy: Always 22 | ports: 23 | - containerPort: 9001 24 | -------------------------------------------------------------------------------- /infra/old/roles/playout/templates/fk-get-filler-videos.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | AssertPathExists={{app_dir}}/app/packages/fkweb/ 3 | 4 | [Service] 5 | Type=oneshot 6 | User={{app_user}} 7 | Group={{app_user}} 8 | WorkingDirectory={{app_dir}}/app/packages/fkweb/ 9 | Environment=SECRET_KEY={{app_secret_key}} 10 | Environment=DATABASE_USER={{app_user}} 11 | Environment=DATABASE_PASS={{app_db_pass}} 12 | Environment=DATABASE_NAME={{app_db_name}} 13 | Environment=DJANGO_SETTINGS_MODULE=fkbeta.settings.production 14 | ExecStart={{app_dir}}/env/bin/python manage.py fill_next_weeks_agenda -v 2 15 | -------------------------------------------------------------------------------- /infra/old/k8s/monitoring/prom-ingress.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: IngressRoute 3 | apiVersion: traefik.containo.us/v1alpha1 4 | metadata: 5 | name: prometheus 6 | namespace: default 7 | spec: 8 | entryPoints: 9 | - websecure 10 | routes: 11 | - match: Host(`prometheus.admin.frikanalen.no`) 12 | kind: Rule 13 | middlewares: 14 | - name: basic-admin-auth 15 | services: 16 | - kind: Service 17 | name: prometheus-stack-kube-prom-prometheus 18 | namespace: monitoring 19 | scheme: http 20 | port: 9090 21 | tls: 22 | certResolver: default 23 | -------------------------------------------------------------------------------- /packages/frontend/modules/state/classes/Store.ts: -------------------------------------------------------------------------------- 1 | import { Manager } from "../types"; 2 | 3 | export class Store { 4 | constructor(protected manager: Manager) {} 5 | 6 | // Constructor-like method without need for super 7 | public make(): void {} 8 | 9 | public init(): void | Promise {} 10 | public reset?(): void; 11 | public hydrate?(data: T): void; 12 | public serialize?(): T; 13 | } 14 | 15 | export const createStoreFactory = 16 | (storeClass: new (manager: Manager) => S) => 17 | (manager: Manager) => 18 | new storeClass(manager); 19 | -------------------------------------------------------------------------------- /infra/old/k8s/monitoring/alertmanager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1alpha1 2 | kind: AlertmanagerConfig 3 | metadata: 4 | name: config-example 5 | namespace: monitoring 6 | labels: 7 | alertmanagerConfig: example 8 | release: prometheus-stack 9 | spec: 10 | route: 11 | groupBy: ['job'] 12 | groupWait: 30s 13 | groupInterval: 5m 14 | repeatInterval: 12h 15 | receiver: 'telegram-example' 16 | receivers: 17 | - name: 'telegram-example' 18 | webhookConfigs: 19 | - sendResolved: true 20 | url: http://telegram-bot/prometheus/-666624603 21 | -------------------------------------------------------------------------------- /packages/frontend/modules/auth/helpers/spawnLoginModal.ts: -------------------------------------------------------------------------------- 1 | import { Manager } from "modules/state/types"; 2 | import React from "react"; 3 | import { LoginModal } from "../components/LoginModal"; 4 | import { createLoginForm } from "../forms/createLoginForm"; 5 | 6 | export const spawnLoginModal = (manager: Manager) => { 7 | const { modalStore } = manager.stores; 8 | const form = createLoginForm(manager); 9 | 10 | modalStore.spawn({ 11 | key: "login", 12 | render: () => React.createElement(LoginModal, { form }), 13 | clickout: true, 14 | dismissOnClickout: true, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/frontend/modules/form/components/FieldsProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, PropsWithChildren } from "react" 2 | import { FieldsType } from "../classes/ObservableForm" 3 | import React from "react" 4 | 5 | export const fieldsContext = createContext(undefined) 6 | const { Provider } = fieldsContext 7 | 8 | export type FieldsProviderProps = PropsWithChildren<{ 9 | fields: FieldsType 10 | }> 11 | 12 | export function FieldsProvider(props: FieldsProviderProps) { 13 | const { fields, children } = props 14 | 15 | return {children} 16 | } 17 | -------------------------------------------------------------------------------- /packages/frontend/modules/video/types.ts: -------------------------------------------------------------------------------- 1 | import { OrganizationData } from "modules/organization/resources/Organization"; 2 | 3 | export type VideoData = { 4 | id: number; 5 | name: string; 6 | header: string; 7 | organization: OrganizationData; 8 | ogvUrl: string; 9 | createdTime: string; 10 | files: { 11 | largeThumb: string; 12 | }; 13 | }; 14 | 15 | export type VideoCategoryData = { 16 | id: number; 17 | name: string; 18 | desc: string; 19 | videocount: number; 20 | }; 21 | 22 | export type VideoUploadTokenData = { 23 | uploadToken: string; 24 | uploadUrl: string; 25 | }; 26 | -------------------------------------------------------------------------------- /infra/old/k8s/ingress/frikanalen_middlewares.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: Middleware 3 | metadata: 4 | name: basic-admin-auth 5 | spec: 6 | basicAuth: 7 | secret: basic-auth-secret 8 | --- 9 | apiVersion: traefik.containo.us/v1alpha1 10 | kind: Middleware 11 | metadata: 12 | name: fklan-ipwhitelist 13 | spec: 14 | ipWhiteList: 15 | sourceRange: 16 | - 192.168.3.0/24 17 | --- 18 | apiVersion: traefik.containo.us/v1alpha1 19 | kind: Middleware 20 | metadata: 21 | name: scheme-redirect 22 | spec: 23 | redirectScheme: 24 | scheme: https 25 | permanent: true 26 | -------------------------------------------------------------------------------- /packages/utils/prom-check-video-stream/README.md: -------------------------------------------------------------------------------- 1 | Transport stream analyzer for prometheus 2 | 3 | So far, all this does is sleep, grab a chunk of TS data from the cubemap on simula, and 4 | log mean audio level. 5 | 6 | Requires tsanalyze and ffmpeg. 7 | 8 | ## Install 9 | 10 | ``` 11 | apt-get install virtualenv 12 | virtualenv -p python3 env 13 | . env/bin/activate 14 | pip install -r requirements.txt 15 | ``` 16 | 17 | ## TODO 18 | 19 | * Better deployment (setup.py?) 20 | * Configurability 21 | * systemd service 22 | * Video motion analysis (still image detection) 23 | * ~~Transport stream statistics (tsanalyze?)~~ 24 | -------------------------------------------------------------------------------- /packages/frontend/modules/auth/helpers/spawnRegisterModal.ts: -------------------------------------------------------------------------------- 1 | import { Manager } from "modules/state/types"; 2 | import React from "react"; 3 | import { RegisterModal } from "../components/RegisterModal"; 4 | import { createRegisterForm } from "../forms/createRegisterForm"; 5 | 6 | export const spawnRegisterModal = (manager: Manager) => { 7 | const { modalStore } = manager.stores; 8 | const form = createRegisterForm(manager); 9 | 10 | modalStore.spawn({ 11 | key: "register", 12 | render: () => React.createElement(RegisterModal, { form }), 13 | clickout: true, 14 | dismissOnClickout: true, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/SVGIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as icons from "../icons"; 2 | import { IconType } from "../types"; 3 | import React from "react"; 4 | 5 | export type SVGIconProps = { 6 | className?: string; 7 | name: IconType; 8 | }; 9 | 10 | export function SVGIcon(props: SVGIconProps) { 11 | const { name, className } = props; 12 | 13 | if (!icons[name]) return null; 14 | 15 | return React.cloneElement(icons[name], { 16 | className, 17 | width: "100%", 18 | height: "100%", 19 | }); 20 | } 21 | 22 | export type SVGIconWithProps = (props: SVGIconProps & T) => JSX.Element; 23 | -------------------------------------------------------------------------------- /packages/utils/stills-generator/README.md: -------------------------------------------------------------------------------- 1 | # stills-generator 2 | ![Build stills generator microservice](https://github.com/frikanalen/Frikanalen/workflows/Build%20stills%20generator%20microservice/badge.svg) 3 | 4 | This microservice generates a poster with text, which is used to generate 5 | stills during eg. live transmissions, generally uploaded to the ATEM production 6 | switcher. 7 | 8 | ## Usage 9 | 10 | 11 | `GET /getPoster.png?text=foo&heading=bar` 12 | 13 | `POST /getPoster.png <- {"text": "foo", "heading": "bar"}` 14 | 15 | Returns a 1280x720 PNG file. 16 | 17 | ## Todo 18 | 19 | - Configurable graphical profiles 20 | -------------------------------------------------------------------------------- /infra/playbooks/files/zfs-monitoring.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: PrometheusRule 3 | metadata: 4 | name: zfs-health-monitor 5 | namespace: monitoring 6 | spec: 7 | groups: 8 | - name: zfs_health_rules 9 | rules: 10 | - alert: ZfsOfflinePool 11 | expr: node_zfs_zpool_state{state!="online"} > 0 12 | for: 1m 13 | labels: 14 | severity: critical 15 | annotations: 16 | summary: "ZFS offline pool (instance {{ $labels.instance }})" 17 | description: "A ZFS zpool is in a unexpected state: {{ $labels.state }}.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" 18 | -------------------------------------------------------------------------------- /packages/frontend/modules/core/components/Body.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React from "react"; 3 | import { mainContentStyle } from "../styles/mainContentStyle"; 4 | 5 | export const Container = styled.div` 6 | display: flex; 7 | justify-content: center; 8 | 9 | margin-top: 32px; 10 | width: 100%; 11 | `; 12 | 13 | const Content = styled.main` 14 | ${mainContentStyle} 15 | `; 16 | 17 | export function Body(props: React.PropsWithChildren<{}>) { 18 | const { children } = props; 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/utils/atem-control/src/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from "winston"; 2 | export const getLogger = () => { 3 | if (process.env["NODE_ENV"] === "production") { 4 | return winston.createLogger({ 5 | level: "info", 6 | format: winston.format.json(), 7 | transports: [new winston.transports.Console()], 8 | }); 9 | } else { 10 | return winston.createLogger({ 11 | level: "info", 12 | format: winston.format.combine( 13 | winston.format.colorize(), 14 | winston.format.simple() 15 | ), 16 | transports: [new winston.transports.Console()], 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /infra/old/roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include_vars: users.yml 3 | - name: "Create user accounts" 4 | user: 5 | name: "{{ item.username }}" 6 | groups: "sudo,www-data,fkweb" 7 | shell: "/bin/bash" 8 | with_items: "{{ users }}" 9 | - name: "Add authorized keys" 10 | authorized_key: 11 | user: "{{ item.username }}" 12 | key: "{{ item.ssh_keys }}" 13 | with_items: "{{ users }}" 14 | - name: "Allow admin users to sudo without a password" 15 | lineinfile: 16 | dest: "/etc/sudoers" # path: in version 2.3 17 | state: "present" 18 | regexp: "^%sudo" 19 | line: "%sudo ALL=(ALL) NOPASSWD: ALL" 20 | -------------------------------------------------------------------------------- /packages/frontend/modules/playout/helpers/spawnTextSlideModal.ts: -------------------------------------------------------------------------------- 1 | import { Manager } from "modules/state/types"; 2 | import React from "react"; 3 | import { TextSlideModal } from "../components/TextSlideModal"; 4 | import { createTextSlideForm } from "../forms/createTextSlideForm"; 5 | 6 | export const spawnTextSlideModal = (manager: Manager) => { 7 | const { modalStore } = manager.stores; 8 | const form = createTextSlideForm(manager); 9 | 10 | modalStore.spawn({ 11 | key: "textslide", 12 | render: () => React.createElement(TextSlideModal, { form }), 13 | clickout: true, 14 | dismissOnClickout: true, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /infra/playbooks/files/kibana-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | annotations: 5 | traefik.ingress.kubernetes.io/router.entrypoints: websecure 6 | traefik.ingress.kubernetes.io/router.middlewares: default-admin-auth@kubernetescrd 7 | name: kibana 8 | namespace: kube-system 9 | spec: 10 | ingressClassName: traefik 11 | rules: 12 | - host: kibana.admin.frikanalen.no 13 | http: 14 | paths: 15 | - backend: 16 | service: 17 | name: kibana-logging 18 | port: 19 | number: 5601 20 | path: / 21 | pathType: Prefix 22 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { PropsWithChildren } from "react"; 3 | import { linkStyle } from "../styles/linkStyle"; 4 | 5 | const Container = styled.a` 6 | ${linkStyle} 7 | `; 8 | 9 | export type ExternalLinkProps = PropsWithChildren<{ 10 | className?: string; 11 | href: string; 12 | }>; 13 | 14 | export function ExternalLink(props: ExternalLinkProps) { 15 | const { className, href, children } = props; 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/frontend/types/emotion.d.ts: -------------------------------------------------------------------------------- 1 | import "@emotion/react"; 2 | 3 | declare module "@emotion/react" { 4 | export interface Theme { 5 | color: { 6 | background: string; 7 | card: string; 8 | accent: string; 9 | secondAccent: string; 10 | thirdAccent: string; 11 | divider: string; 12 | overlay: string; 13 | }; 14 | stateColor: { 15 | success: string; 16 | warning: string; 17 | danger: string; 18 | tip: string; 19 | }; 20 | fontColor: { 21 | overlay: string; 22 | normal: string; 23 | muted: string; 24 | subdued: string; 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/schedule-service/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask import Response 3 | from playout import playout_schedule 4 | import jsonpickle 5 | import json 6 | 7 | app = Flask(__name__) 8 | 9 | @app.route('/playout') 10 | def playoutschedule(): 11 | video_list = playout_schedule() 12 | return Response(\ 13 | jsonpickle.encode(video_list,unpicklable=False), 14 | mimetype='application/json' 15 | ) 16 | 17 | 18 | if __name__=="__main__": 19 | graphics = [] 20 | video_list = playout_schedule() 21 | print(json.dumps(json.loads(jsonpickle.encode(video_list,unpicklable=False)), indent=2)) 22 | 23 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-legacy/s3_policies.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [{ 4 | "Effect": "Allow", 5 | "Principal": {"AWS": ["arn:aws:iam:::user/upload-ingest"]}, 6 | "Action": "s3:GetObject", 7 | "Resource": [ 8 | "arn:aws:s3:::incoming/*" 9 | ] 10 | }, 11 | { 12 | "Effect": "Allow", 13 | "Principal": {"AWS": ["arn:aws:iam:::user/upload-ingest"]}, 14 | "Action": "s3:ListBucket", 15 | "Resource": [ 16 | "arn:aws:s3:::incoming" 17 | ] 18 | }] 19 | } 20 | -------------------------------------------------------------------------------- /packages/utils/ingest/copy-to-cloudflare/s3_policies.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [{ 4 | "Effect": "Allow", 5 | "Principal": {"AWS": ["arn:aws:iam:::user/upload-ingest"]}, 6 | "Action": "s3:GetObject", 7 | "Resource": [ 8 | "arn:aws:s3:::incoming/*" 9 | ] 10 | }, 11 | { 12 | "Effect": "Allow", 13 | "Principal": {"AWS": ["arn:aws:iam:::user/upload-ingest"]}, 14 | "Action": "s3:ListBucket", 15 | "Resource": [ 16 | "arn:aws:s3:::incoming" 17 | ] 18 | }] 19 | } 20 | -------------------------------------------------------------------------------- /infra/old/k8s/helm-values/README: -------------------------------------------------------------------------------- 1 | These are the values files for the Helm packages we use. 2 | 3 | These are documented here. Ideally these should probably be integrated into our Ansible setup! 4 | 5 | See eg. https://www.ansible.com/blog/automating-helm-using-ansible 6 | 7 | helm install -f kube-prometheus-stack.yaml prometheus-stack prometheus-community/kube-prometheus-stack --namespace monitoring 8 | helm install -f prometheus-postgres-exporter.yaml prometheus-postgres-exporter prometheus-community/prometheus-postgres-exporter 9 | 10 | helm install -n beta -f prometheus-postgres-exporter-beta.yaml pg-prom-exporter prometheus-community/prometheus-postgres-exporter 11 | -------------------------------------------------------------------------------- /infra/old/charts/frontend/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | kind: IngressRoute 2 | apiVersion: traefik.containo.us/v1alpha1 3 | metadata: 4 | name: {{ .Release.Name }}-frontend 5 | namespace: {{ .Values.namespace }} 6 | spec: 7 | tls: 8 | certResolver: default 9 | entryPoints: 10 | - websecure 11 | routes: 12 | - match: Host(`{{ .Values.domain }}`) 13 | kind: Rule 14 | services: 15 | - name: {{ .Release.Name }}-frontend 16 | kind: Service 17 | scheme: http 18 | port: 3000 19 | sticky: 20 | cookie: 21 | httpOnly: true 22 | name: fk-load-balancer 23 | secure: true 24 | sameSite: none 25 | -------------------------------------------------------------------------------- /packages/frontend/modules/auth/forms/createLoginForm.ts: -------------------------------------------------------------------------------- 1 | import { ObservableForm } from "modules/form/classes/ObservableForm"; 2 | import { string } from "modules/form/fields/string"; 3 | import { Manager } from "modules/state/types"; 4 | 5 | export const createLoginForm = (manager: Manager) => { 6 | return new ObservableForm( 7 | { 8 | email: string().required(), 9 | password: string() 10 | .required() 11 | .min(6, "Passord må være minst 6 tegn") 12 | .max(64, "Imponerende, men ditt passord må være maksimalt 64 tegn"), 13 | }, 14 | manager 15 | ); 16 | }; 17 | 18 | export type LoginForm = ReturnType; 19 | -------------------------------------------------------------------------------- /packages/frontend/modules/organization/helpers/fetchBrregData.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export type BrregAddress = { 4 | adresse: string[]; 5 | kommune: string; 6 | postnummer: string; 7 | poststed: string; 8 | }; 9 | 10 | export type BrregData = { 11 | navn: string; 12 | postadresse: BrregAddress; 13 | forretningsadresse: BrregAddress; 14 | hjemmeside: string; 15 | }; 16 | 17 | export const fetchBrregData = async (organization: string) => { 18 | try { 19 | const { data } = await axios.get(`https://data.brreg.no/enhetsregisteret/api/enheter/${organization}`); 20 | return data; 21 | } catch { 22 | return; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /packages/utils/monitoring-stream-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monitoring-stream-server", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node dist/server.js", 8 | "dev": "ts-node-dev src/server.ts", 9 | "prettier": "prettier -w src" 10 | }, 11 | "dependencies": { 12 | "dgram": "^1.0.1", 13 | "tslog": "^3.3.3", 14 | "ws": "^7.4.6" 15 | }, 16 | "devDependencies": { 17 | "@tsconfig/node16-strictest": "^1.0.1", 18 | "@types/node": "^17.0.40", 19 | "@types/ws": "^8.5.3", 20 | "prettier": "^2.6.2", 21 | "ts-node-dev": "^2.0.0", 22 | "typescript": "^4.7.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /infra/old/roles/debian_stock_config/vars/hosts.yml: -------------------------------------------------------------------------------- 1 | static_hosts: 2 | simula.frikanalen.no: 192.168.3.1 3 | billedmiks: 10.3.2.1 4 | borch.frikanalen.no: 192.168.3.8 5 | obe-hd.frikanalen.no: 192.168.3.10 6 | ipmi.file01: 192.168.3.11 7 | ipmi.tx1: 192.168.3.20 8 | ipmi.tx2: 192.168.3.21 9 | ipmi.tx3: 192.168.3.22 10 | fw1.frikanalen.no: 192.168.3.240 11 | tx1.frikanalen.no: 192.168.3.33 12 | tx2.frikanalen.no: 192.168.3.34 13 | tx3.frikanalen.no: 192.168.3.35 14 | tx4.frikanalen.no: 192.168.3.80 15 | file01.frikanalen.no: 192.168.3.59 16 | ipmi.simula: 192.168.3.61 17 | ipmi.obe-sd: 192.168.3.62 18 | ipmi.dev: 192.168.3.63 19 | ipmi.obe-hd: 192.168.3.70 20 | -------------------------------------------------------------------------------- /packages/frontend/modules/modal/components/PrimaryModal/Header.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | const Container = styled.header` 4 | height: 72px; 5 | 6 | display: flex; 7 | align-items: center; 8 | 9 | padding: 0px 24px; 10 | border-bottom: solid 2px ${(props) => props.theme.color.accent}; 11 | `; 12 | 13 | const Title = styled.h1` 14 | font-weight: 600; 15 | font-size: 1.5em; 16 | 17 | margin-bottom: 0px; 18 | `; 19 | 20 | export type HeaderProps = { 21 | title: string; 22 | }; 23 | 24 | export function Header(props: HeaderProps) { 25 | return ( 26 | 27 | {props.title} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/InternalLink.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import Link from "next/link"; 3 | import React, { PropsWithChildren } from "react"; 4 | import { linkStyle } from "../styles/linkStyle"; 5 | 6 | const Container = styled.a` 7 | ${linkStyle} 8 | `; 9 | 10 | export type InternalLinkProps = PropsWithChildren<{ 11 | className?: string; 12 | href: string; 13 | }>; 14 | 15 | export function InternalLink(props: InternalLinkProps) { 16 | const { className, href, children } = props; 17 | 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | pyreqs=packages/fkweb/requirements-dev.txt packages/fkupload/requirements.txt packages/fkprocess/requirements.txt 2 | 3 | env = env/bin/activate 4 | ifneq ("$(wildcard $(env))","") 5 | VENV = . $(env) && 6 | else 7 | VENV = true && 8 | endif 9 | 10 | env: $(pyreqs) 11 | python3 -m venv env 12 | 13 | .PHONY=requirements test 14 | requirements: 15 | for req in $(pyreqs); do \ 16 | $(VENV) pip install -r $$req; \ 17 | done 18 | 19 | test: 20 | $(VENV) packages/fkweb/manage.py test agenda create fk fkbeta fkvod fkws 21 | $(VENV) python packages/fkprocess/test*.py 22 | $(VENV) packages/fkprocess/test_move_and_process.sh 23 | $(VENV) packages/fkupload/test-fkupload 24 | -------------------------------------------------------------------------------- /packages/frontend/modules/state/helpers/interpretError.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | 3 | export type ErrorType = "unknown" | "not-found" | "unauthorized"; 4 | 5 | export const interpretError = (error: any): ErrorType => { 6 | const { response } = error as AxiosError; 7 | 8 | if (response) { 9 | const { status } = response; 10 | 11 | if (status === 404) return "not-found"; 12 | if (status === 401) return "unauthorized"; 13 | if (status === 500) return "unknown"; 14 | } 15 | 16 | return "unknown"; 17 | }; 18 | 19 | export const errorToStatusMap: Record = { 20 | "not-found": 404, 21 | unauthorized: 401, 22 | unknown: 500, 23 | }; 24 | -------------------------------------------------------------------------------- /infra/old/k8s-legacy/nfs-db-backup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: nfs-pgbackup-pv 5 | spec: 6 | capacity: 7 | storage: 10Gi 8 | volumeMode: Filesystem 9 | accessModes: 10 | - ReadWriteMany 11 | persistentVolumeReclaimPolicy: Retain 12 | storageClassName: nfs 13 | mountOptions: 14 | - hard 15 | - nfsvers=4.1 16 | nfs: 17 | path: /srv/nfs/pgbackups 18 | server: 192.168.3.1 19 | --- 20 | apiVersion: v1 21 | kind: PersistentVolumeClaim 22 | metadata: 23 | name: nfs-pgbackup-pvc 24 | spec: 25 | storageClassName: nfs 26 | accessModes: 27 | - ReadWriteMany 28 | resources: 29 | requests: 30 | storage: 10Gi 31 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { IconType } from "../types"; 3 | import { Button, ButtonProps } from "./Button"; 4 | import { SVGIcon } from "./SVGIcon"; 5 | 6 | const Container = styled(Button)``; 7 | 8 | const Icon = styled(SVGIcon)` 9 | width: 16px; 10 | height: 16px; 11 | `; 12 | 13 | export type IconButtonProps = ButtonProps & { 14 | icon: IconType; 15 | }; 16 | 17 | export function IconButton(props: IconButtonProps) { 18 | const { className, icon, ...rest } = props; 19 | 20 | return ( 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/frontend/.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | # images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | appveyor.yml 29 | circle.yml 30 | codeship-services.yml 31 | codeship-steps.yml 32 | wercker.yml 33 | .tern-project 34 | .gitattributes 35 | .editorconfig 36 | .*ignore 37 | .eslintrc 38 | .jshintrc 39 | .flowconfig 40 | .documentup.json 41 | .yarn-metadata.json 42 | .travis.yml 43 | 44 | # misc 45 | *.md 46 | -------------------------------------------------------------------------------- /infra/playbooks/templates/beta/jukebox.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: jukebox 5 | namespace: "{{ namespace }}" 6 | spec: 7 | schedule: "00 22 * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: jukebox 14 | image: frikanalen/jukebox-v2:latest 15 | env: 16 | - name: FK_API 17 | value: "https://{{ domain }}/api/v2" 18 | - name: FK_API_KEY 19 | valueFrom: 20 | secretKeyRef: 21 | name: fk-api-key 22 | key: FK_API_KEY 23 | restartPolicy: OnFailure 24 | -------------------------------------------------------------------------------- /packages/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frikanalen web frontend 2 | 3 | ![Build frontend package](https://github.com/Frikanalen/frikanalen/workflows/Build%20frontend%20package/badge.svg) 4 | 5 | This is the new frontend, for the old backend. The new one is at Frikanalen/frontend. 6 | 7 | It can be reached at frikanalen.no. 8 | 9 | ## Running locally: 10 | 11 | First, install the dependencies (obviously this requires yarn) 12 | 13 | `yarn install` 14 | 15 | Then there are two profiles to choose from; either 16 | 17 | - `yarn run dev` - if you are using a local Django instance (in which case it will expect to find it at `localhost:8080`), or 18 | - `yarn run staging` - to run the frontend against the production backend API. 19 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/hooks/useAnimation.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export const useAnimation = (frame: () => void, enabled = true) => { 4 | const frameRef = useRef(); 5 | const frameCallbackRef = useRef(frame); 6 | 7 | useEffect(() => { 8 | frameCallbackRef.current = frame; 9 | }); 10 | 11 | useEffect(() => { 12 | const frameHandler = () => { 13 | frameCallbackRef.current(); 14 | frameRef.current = requestAnimationFrame(frameHandler); 15 | }; 16 | 17 | if (enabled) { 18 | frameHandler(); 19 | } 20 | 21 | return () => { 22 | cancelAnimationFrame(frameRef.current as number); 23 | }; 24 | }, [enabled]); 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/playout.yml: -------------------------------------------------------------------------------- 1 | name: Build playout package 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'packages/playout/**' 8 | jobs: 9 | build: 10 | name: Build and push Docker images 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout master 14 | uses: actions/checkout@v2 15 | - name: Build docker 16 | uses: docker/build-push-action@v1.1.0 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | repository: frikanalen/playout 21 | tag_with_ref: true 22 | path: packages/playout 23 | dockerfile: packages/playout/Dockerfile 24 | -------------------------------------------------------------------------------- /infra/old/k8s/rook-ceph/set-policy.bash: -------------------------------------------------------------------------------- 1 | export AWS_ACCESS_KEY_ID=$(kubectl -n beta get secret media -o jsonpath="{['data']['AWS_ACCESS_KEY_ID']}" | base64 --decode) 2 | export AWS_SECRET_ACCESS_KEY=$(kubectl -n beta get secret media -o jsonpath="{['data']['AWS_SECRET_ACCESS_KEY']}" | base64 --decode) 3 | 4 | POLICY=$(cat < { 6 | return new ObservableForm( 7 | { 8 | name: string().required().min(3), 9 | postalAddress: string().required(), 10 | streetAddress: string().required(), 11 | homepage: string().required().url(), 12 | orgnr: string().min(9).max(9).required(), 13 | }, 14 | manager 15 | ); 16 | }; 17 | 18 | export type NewOrganizationForm = ReturnType; 19 | -------------------------------------------------------------------------------- /infra/old/k8s/ingress/old/004-middleware.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | #apiVersion: traefik.containo.us/v1alpha1 3 | #kind: IngressRoute 4 | #metadata: 5 | # name: http-catchall 6 | #spec: 7 | # entryPoints: 8 | # - web 9 | # routes: 10 | # - match: HostRegexp(`{host:.+}`) #Host(`beta.frikanalen.no`) 11 | # kind: Rule 12 | # services: 13 | # - name: traefik-redirect-dummy 14 | # port: 80 15 | # middlewares: 16 | # - name: https-redirect 17 | # priority: 50 18 | #--- 19 | #apiVersion: v1 20 | #kind: Service 21 | #metadata: 22 | # name: traefik-redirect-dummy 23 | # namespace: kube-system 24 | #spec: 25 | # ports: 26 | # - name: web 27 | # port: 80 28 | # protocol: TCP 29 | # targetPort: web 30 | # type: ClusterIP 31 | -------------------------------------------------------------------------------- /packages/frontend/modules/modal/components/PrimaryModal/Container.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { PropsWithChildren } from "react"; 3 | 4 | const Root = styled.aside` 5 | width: 460px; 6 | max-width: 100%; 7 | max-height: 100vh; 8 | 9 | border-radius: 3px; 10 | 11 | box-shadow: 2px 2px 11px 2px rgba(0, 0, 0, 0.1); 12 | background: ${(props) => props.theme.color.card}; 13 | 14 | display: flex; 15 | flex-direction: column; 16 | 17 | @media (max-width: 460px) { 18 | border-radius: 0px; 19 | } 20 | `; 21 | 22 | export function Container(props: PropsWithChildren<{ className?: string }>) { 23 | const { children, ...rest } = props; 24 | 25 | return {children}; 26 | } 27 | -------------------------------------------------------------------------------- /packages/frontend/modules/state/classes/Resource.ts: -------------------------------------------------------------------------------- 1 | import { observable } from "mobx"; 2 | import { Manager } from "../types"; 3 | 4 | /** Represents a resource containing data that may be fetched later, or that can fail */ 5 | export class Resource { 6 | public data: D; 7 | public rawData: D; 8 | 9 | public constructor(data: D, protected manager: Manager) { 10 | this.data = observable(data as any); // FIXME: Huh? 11 | this.rawData = data; 12 | } 13 | 14 | public populate(newData: Partial) { 15 | this.data = { ...this.data, ...newData }; 16 | this.rawData = { ...this.rawData, ...newData }; 17 | } 18 | } 19 | 20 | export type ResourceFactory> = (data: R["data"], manager: Manager) => R; 21 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/hooks/useStatusLine.ts: -------------------------------------------------------------------------------- 1 | import { getUniqueId } from "modules/state/helpers/getUniqueId"; 2 | import { useCallback, useMemo, useState } from "react"; 3 | import { StatusType } from "../components/StatusLine"; 4 | 5 | export const useStatusLine = () => { 6 | const [status, setStatus] = useState<[StatusType, string, number]>(["info", "", -1]); 7 | 8 | const set = useCallback((type: StatusType, message: string) => { 9 | setStatus([type, message, getUniqueId()]); 10 | }, []); 11 | 12 | const [type, message, fingerprint] = status; 13 | 14 | const props = useMemo(() => { 15 | return { type, message, fingerprint }; 16 | }, [type, message, fingerprint]); 17 | 18 | return [props, set] as const; 19 | }; 20 | -------------------------------------------------------------------------------- /infra/old/roles/upload-app/templates/fkupload.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | AssertPathExists={{upload_app_dir}} 3 | 4 | [Service] 5 | User={{app_user}} 6 | Group={{app_user}} 7 | PIDFile=/run/fkupload/gunicorn.pid 8 | WorkingDirectory={{upload_app_dir}} 9 | Environment=FK_API={{fk_api_url}} 10 | Environment=UPLOAD_DIR={{upload_dir}} 11 | Environment=FINISHED_DIR={{upload_finished_dir}} 12 | Environment=FK_TOKEN={{fk_auth_token}} 13 | ExecStart={{app_dir}}/env/bin/gunicorn --pid /run/fkupload/gunicorn.pid fkupload:app -b {{app_bind}} 14 | Restart=always 15 | PrivateTmp=true 16 | RuntimeDirectory=fkupload 17 | #NoNewPrivileges=true 18 | ExecReload=/bin/kill -s TERM $MAINPID 19 | ExecStop=/bin/kill -s TERM $MAINPID 20 | 21 | [Install] 22 | WantedBy=default.target 23 | -------------------------------------------------------------------------------- /packages/frontend/modules/user/forms/createProfileForm.ts: -------------------------------------------------------------------------------- 1 | import { ObservableForm } from "modules/form/classes/ObservableForm"; 2 | import { string } from "modules/form/fields/string"; 3 | import { Manager } from "modules/state/types"; 4 | import { User } from "../schemas"; 5 | 6 | export const createProfileForm = (user: User, manager: Manager) => { 7 | return new ObservableForm( 8 | { 9 | firstName: string({ 10 | value: user.firstName, 11 | }), 12 | lastName: string({ 13 | value: user.lastName, 14 | }), 15 | phoneNumber: string({ 16 | value: user.phoneNumber, 17 | }), 18 | }, 19 | manager 20 | ); 21 | }; 22 | 23 | export type ProfileForm = ReturnType; 24 | -------------------------------------------------------------------------------- /packages/frontend/modules/state/components/ListTail.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { ScrollTrigger, ScrollTriggerProps } from "modules/ui/components/ScrollTrigger"; 3 | import { List } from "../classes/List"; 4 | 5 | export type ListTailProps = { 6 | list: List; 7 | } & Omit; 8 | 9 | export function ListTail(props: ListTailProps) { 10 | const { list, ...rest } = props; 11 | 12 | const safelyFetch = useCallback( 13 | async (recheck: () => void) => { 14 | if (list.status === "idle" && list.hasMore) { 15 | await list.more(); 16 | recheck(); 17 | } 18 | }, 19 | [list] 20 | ); 21 | 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /infra/old/charts/frontend/values.yaml: -------------------------------------------------------------------------------- 1 | namespace: fk 2 | replicas: 2 3 | minReadySeconds: 5 4 | domain: frikanalen.no 5 | image: 6 | repository: frikanalen/frontend-v2 7 | tag: latest 8 | pullPolicy: Always 9 | env: 10 | - name: FK_MEDIA 11 | value: https://{{ .Values.domain }}/media 12 | - name: FK_GRAPHQL 13 | value: https://{{ .Values.domain }}/graphql 14 | - name: FK_API 15 | value: https://{{ .Values.domain }}/api/v2 16 | - name: FK_UPLOAD 17 | value: https://{{ .Values.domain }}/api/v2 18 | probe: 19 | initialDelaySeconds: 10 20 | readinessProbe: 21 | httpGet: 22 | path: /healthz 23 | port: 3000 24 | livenessProbe: 25 | httpGet: 26 | path: /healthz 27 | port: 3000 28 | service: 29 | type: ClusterIP 30 | -------------------------------------------------------------------------------- /infra/playbooks/templates/database/nfs-backup-volume.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: nfs-pgbackup-pv 5 | spec: 6 | capacity: 7 | storage: 10Gi 8 | volumeMode: Filesystem 9 | accessModes: 10 | - ReadWriteMany 11 | persistentVolumeReclaimPolicy: Retain 12 | storageClassName: nfs 13 | mountOptions: 14 | - hard 15 | - nfsvers=4.1 16 | nfs: 17 | path: "{{ backups_nfs_path }}" 18 | server: "{{ backups_nfs_host }}" 19 | --- 20 | apiVersion: v1 21 | kind: PersistentVolumeClaim 22 | metadata: 23 | name: nfs-pgbackup-pvc 24 | namespace: "{{ namespace }}" 25 | spec: 26 | storageClassName: nfs 27 | accessModes: 28 | - ReadWriteMany 29 | resources: 30 | requests: 31 | storage: 10Gi 32 | -------------------------------------------------------------------------------- /.github/workflows/schedule-service.yml: -------------------------------------------------------------------------------- 1 | name: Build schedule-service package 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'packages/schedule-service/**' 8 | jobs: 9 | build: 10 | name: Build and push Docker images 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout master 14 | uses: actions/checkout@v2 15 | - name: Build docker 16 | uses: docker/build-push-action@v1.1.0 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | repository: frikanalen/schedule-service 21 | tag_with_ref: true 22 | path: packages/schedule-service 23 | dockerfile: packages/schedule-service/Dockerfile 24 | -------------------------------------------------------------------------------- /.github/workflows/atem-control-service.yml: -------------------------------------------------------------------------------- 1 | name: Build ATEM control service 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'packages/utils/atem-control/**' 8 | jobs: 9 | build: 10 | name: Build and push Docker images 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout master 14 | uses: actions/checkout@v2 15 | - name: ATEM control service 16 | uses: docker/build-push-action@v1.1.0 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | repository: frikanalen/atem-control 21 | tag_with_ref: true 22 | path: packages/utils/atem-control 23 | dockerfile: packages/utils/atem-control/Dockerfile 24 | -------------------------------------------------------------------------------- /infra/old/k8s/ingress/old/001-persistent-cert-storage.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: PersistentVolume 3 | apiVersion: v1 4 | metadata: 5 | name: traefik-acme-storage 6 | namespace: kube-system 7 | labels: 8 | type: local 9 | spec: 10 | storageClassName: manual 11 | capacity: 12 | storage: 100Mi 13 | accessModes: 14 | - ReadWriteOnce 15 | hostPath: 16 | path: "/data/traefik-acme-storage" 17 | type: DirectoryOrCreate 18 | --- 19 | apiVersion: v1 20 | kind: PersistentVolumeClaim 21 | metadata: 22 | labels: 23 | component: "server" 24 | app: traefik 25 | name: traefik-acme-storage 26 | namespace: kube-system 27 | spec: 28 | storageClassName: manual 29 | accessModes: 30 | - ReadWriteOnce 31 | resources: 32 | requests: 33 | storage: "100Mi" 34 | -------------------------------------------------------------------------------- /infra/old/k8s/rook-ceph/s3-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: rook-ceph-rgw 6 | ceph_daemon_id: media-store 7 | ceph_daemon_type: rgw 8 | rgw: media-store 9 | rook_cluster: rook-ceph 10 | rook_object_store: media-store 11 | name: rook-s3-service 12 | namespace: rook-ceph 13 | spec: 14 | externalIPs: 15 | - 192.168.3.100 16 | externalTrafficPolicy: Cluster 17 | ports: 18 | - name: web 19 | nodePort: 30460 20 | port: 8085 21 | protocol: TCP 22 | targetPort: 8080 23 | selector: 24 | app: rook-ceph-rgw 25 | ceph_daemon_id: media-store 26 | rgw: media-store 27 | rook_cluster: rook-ceph 28 | rook_object_store: media-store 29 | sessionAffinity: None 30 | type: LoadBalancer 31 | -------------------------------------------------------------------------------- /infra/old/k8s/multiviewer-streaming/002-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: janus-config 5 | namespace: multiviewer 6 | data: 7 | ffmpeg_options: -probesize 100M -threads 10 -strict -2 8 | source_url: http://192.168.3.1:9094/tx2.ts 9 | encoding_options: >- 10 | -pix_fmt yuv420p -filter_complex [0:v]split=2[s1][s2] 11 | 12 | -map 0:1 -codec:a aac -f mpegts udp://127.0.0.1:9000 13 | 14 | -map [s1] -crf 25 -b:v 1000k -maxrate:v:0 1500k 15 | -profile:v baseline 16 | -deadline realtime -codec:v libx264 -preset fast 17 | -f mpegts udp://127.0.0.1:9002 18 | 19 | -map [s2] -crf 25 -b:v 2500k -maxrate:v:0 2500k -speed 7 20 | -deadline realtime -codec:v libx264 -preset fast 21 | -f mpegts udp://127.0.0.1:9003 22 | -------------------------------------------------------------------------------- /packages/frontend/modules/modal/components/ModalOverlay.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { observer } from "mobx-react-lite"; 3 | import { ModalRenderer } from "./ModalRenderer"; 4 | import { useStores } from "modules/state/manager"; 5 | import { Transition, TransitionGroup } from "react-transition-group"; 6 | 7 | export const ModalOverlay = observer(() => { 8 | const { modalStore } = useStores(); 9 | const items = modalStore.items.filter((item) => item.visible); 10 | 11 | return ( 12 | 13 | {items.map((item) => ( 14 | 15 | {(status) => } 16 | 17 | ))} 18 | 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/utils/prom-check-video-stream/snapshot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | 4 | class Snapshot: 5 | filename = 'fk_test.ts' 6 | 7 | async def update(self): 8 | async with aiohttp.ClientSession() as session: 9 | async with session.get('http://192.168.3.1:9094/frikanalen.ts') as resp: 10 | with open(self.filename, 'wb') as fd: 11 | while fd.tell() <= 10000000: 12 | chunk = await resp.content.read(1024) 13 | if not chunk: 14 | break 15 | fd.write(chunk) 16 | 17 | if __name__ == '__main__': 18 | loop = asyncio.new_event_loop() 19 | asyncio.set_event_loop(loop) 20 | result = loop.run_until_complete(Snapshot.update()) 21 | -------------------------------------------------------------------------------- /packages/frontend/modules/form/fields/array.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ObservableFormField, 3 | ObservableFormFieldOptions, 4 | } from "../classes/ObservableFormField" 5 | 6 | export class ObservableArrayField extends ObservableFormField { 7 | public min(min: number, message = `Minimum ${min} items`) { 8 | this.validators.add(async (value) => { 9 | return value.length < min ? message : "" 10 | }) 11 | 12 | return this 13 | } 14 | 15 | public max(max: number, message = `Maximum ${max} items`) { 16 | this.validators.add(async (value) => { 17 | return value.length > max ? message : "" 18 | }) 19 | 20 | return this 21 | } 22 | } 23 | 24 | export const array = (options: ObservableFormFieldOptions) => { 25 | return new ObservableArrayField(options) 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/stills-generator.yml: -------------------------------------------------------------------------------- 1 | name: Build stills generator microservice 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'packages/utils/stills-generator/**' 8 | jobs: 9 | build: 10 | name: Build and push Docker images 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout master 14 | uses: actions/checkout@v2 15 | - name: Stills generator 16 | uses: docker/build-push-action@v1.1.0 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | repository: frikanalen/stills-generator 21 | tag_with_ref: true 22 | path: packages/utils/stills-generator 23 | dockerfile: packages/utils/stills-generator/Dockerfile 24 | -------------------------------------------------------------------------------- /packages/frontend/modules/schedule/helpers/humanizeScheduleItemDate.ts: -------------------------------------------------------------------------------- 1 | import { differenceInMinutes, differenceInSeconds, format, isToday } from "date-fns"; 2 | import { nb } from "date-fns/locale"; 3 | 4 | export const humanizeScheduleItemDate = (date: Date) => { 5 | const now = new Date(); 6 | const secondDifference = differenceInSeconds(date, now); 7 | 8 | if (secondDifference <= 0) { 9 | return "nå"; 10 | } 11 | 12 | if (secondDifference < 60) { 13 | return `${secondDifference} sek`; 14 | } 15 | 16 | if (differenceInMinutes(date, now) < 60) { 17 | return `${Math.round(secondDifference / 60)} min`; 18 | } 19 | 20 | if (isToday(date)) { 21 | return format(date, "HH:mm", { locale: nb }); 22 | } 23 | 24 | return format(date, "HH:mm - d. MMM", { locale: nb }); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/utils/obe-service/README.md: -------------------------------------------------------------------------------- 1 | # fk-obe 2 | 3 | These are the systemd configuration files and a support script to enable us to 4 | launch open broadcast encoder using systemd so the system can boot at load time. 5 | 6 | ## Installation 7 | 8 | As root: 9 | ~~~~ 10 | apt-get install python3-pexpect 11 | adduser --system --no-create-home --shell /bin/nologin fk-obe 12 | install -m 644 fk-obe.service /etc/systemd/system 13 | install -m 755 fk-obe /usr/local/bin 14 | install -m 644 fk-obe.conf /etc 15 | systemctl daemon-reload 16 | ~~~~ 17 | 18 | ## Todo: 19 | 20 | * Move more configuration parameters into fk-obe.conf; ideally supporting 21 | preset roles which can be used by name and defined in the configuration 22 | file 23 | 24 | * Package this script as a Debian package for easier deployment 25 | 26 | -------------------------------------------------------------------------------- /infra/old/k8s/monitoring/kube_alerts.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Skrev denne reglene for å feilsøke en issue hvorved simula ikke får sine CSRs approved. 3 | # 4 | apiVersion: monitoring.coreos.com/v1 5 | kind: PrometheusRule 6 | metadata: 7 | name: csr-alert-rule 8 | namespace: monitoring 9 | labels: 10 | app: prometheus-operator 11 | release: monitoring 12 | spec: 13 | groups: 14 | - name: csr_alert_rules 15 | rules: 16 | - alert: HighNumberOfCertificateSigningRequests 17 | expr: sum(kube_certificatesigningrequest_created) > 5 18 | for: 5m 19 | labels: 20 | severity: warning 21 | annotations: 22 | summary: "High number of Certificate Signing Requests" 23 | description: "There are more than 5 Certificate Signing Requests in the cluster (current value: {{ $value }})" 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/monitoring-stream-server.yml: -------------------------------------------------------------------------------- 1 | name: Build monitoring stream server 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'packages/utils/monitoring-stream-server/**' 8 | jobs: 9 | build: 10 | name: Build and push Docker images 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout master 14 | uses: actions/checkout@v2 15 | - name: Build docker 16 | uses: docker/build-push-action@v1.1.0 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | repository: frikanalen/monitoring-stream-ws 21 | tag_with_ref: true 22 | path: packages/utils/monitoring-stream-server 23 | dockerfile: packages/utils/monitoring-stream-server/Dockerfile 24 | -------------------------------------------------------------------------------- /infra/old/k8s-dev/coredns-config.yaml: -------------------------------------------------------------------------------- 1 | kind: ConfigMap 2 | metadata: 3 | labels: 4 | k8s-app: kube-dns 5 | name: coredns 6 | namespace: kube-system 7 | apiVersion: v1 8 | data: 9 | Corefile: | 10 | .:53 { 11 | errors 12 | health { 13 | lameduck 5s 14 | } 15 | ready 16 | 17 | rewrite name fk.dev.local ingress.traefik.svc.cluster.local 18 | 19 | log . { 20 | class error 21 | } 22 | 23 | kubernetes cluster.local in-addr.arpa ip6.arpa { 24 | pods insecure 25 | fallthrough in-addr.arpa ip6.arpa 26 | } 27 | 28 | prometheus :9153 29 | 30 | forward . 8.8.8.8 8.8.4.4 31 | cache 30 32 | loop 33 | 34 | reload 35 | loadbalance 36 | } 37 | -------------------------------------------------------------------------------- /infra/playbooks/templates/beta/epg.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: Middleware 3 | metadata: 4 | name: epgxml-prefix 5 | namespace: "{{ namespace }}" 6 | spec: 7 | addPrefix: 8 | prefix: /epg 9 | --- 10 | kind: IngressRoute 11 | apiVersion: traefik.containo.us/v1alpha1 12 | metadata: 13 | name: toches-xmltv 14 | namespace: "{{ namespace }}" 15 | spec: 16 | entryPoints: 17 | - websecure 18 | routes: 19 | - match: Host(`{{ domain }}` && PathPrefix(`/xmltv`) 20 | kind: Rule 21 | middlewares: 22 | - name: strip-www 23 | namespace: "{{ namespace }}" 24 | - name: epgxml-prefix 25 | namespace: "{{ namespace }}" 26 | services: 27 | - kind: Service 28 | name: toches 29 | scheme: http 30 | port: 80 31 | tls: 32 | certResolver: default 33 | -------------------------------------------------------------------------------- /infra/old/k8s-dev/block-storage-class.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: storage.k8s.io/v1 2 | kind: StorageClass 3 | metadata: 4 | name: rook-ceph-block 5 | parameters: 6 | clusterID: rook-ceph 7 | csi.storage.k8s.io/controller-expand-secret-name: rook-csi-rbd-provisioner 8 | csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph 9 | csi.storage.k8s.io/fstype: ext4 10 | csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node 11 | csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph 12 | csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner 13 | csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph 14 | imageFeatures: layering 15 | imageFormat: "2" 16 | pool: replicapool 17 | provisioner: rook-ceph.rbd.csi.ceph.com 18 | reclaimPolicy: Delete 19 | allowVolumeExpansion: true 20 | volumeBindingMode: Immediate 21 | -------------------------------------------------------------------------------- /packages/frontend/modules/core/components/AspectContainer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { PropsWithChildren } from "react"; 3 | 4 | const Container = styled.div<{ width: number; height: number }>` 5 | position: relative; 6 | padding-top: ${(props) => (props.height / props.width) * 100}%; 7 | `; 8 | 9 | const Inner = styled.div` 10 | position: absolute; 11 | top: 0px; 12 | left: 0px; 13 | right: 0px; 14 | bottom: 0px; 15 | `; 16 | 17 | export type AspectContainerProps = PropsWithChildren<{ 18 | width: number; 19 | height: number; 20 | }>; 21 | 22 | export function AspectContainer(props: AspectContainerProps) { 23 | const { children, width, height } = props; 24 | 25 | return ( 26 | 27 | {children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /infra/old/k8s/atem-control/001-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: atem-control 5 | namespace: default 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: atem-control 10 | minReadySeconds: 5 11 | template: 12 | metadata: 13 | labels: 14 | app: atem-control 15 | spec: 16 | containers: 17 | - name: atem-control 18 | image: frikanalen/atem-control:latest 19 | env: 20 | - name: ATEM_HOST 21 | value: 10.3.2.1 22 | - name: LISTEN_PORT 23 | value: "80" 24 | - name: FK_API_URL 25 | value: "https://frikanalen.no/api" 26 | - name: FK_APIV2_URL 27 | value: "https://beta.frikanalen.no/api/v2" 28 | ports: 29 | - name: web 30 | containerPort: 80 31 | -------------------------------------------------------------------------------- /packages/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS deps 2 | RUN apk add --no-cache libc6-compat 3 | WORKDIR /app 4 | 5 | COPY package.json yarn.lock ./ 6 | RUN yarn install 7 | 8 | FROM node:18-alpine AS builder 9 | WORKDIR /app 10 | COPY --from=deps /app/node_modules ./node_modules 11 | COPY . . 12 | 13 | ENV NEXT_TELEMETRY_DISABLED 1 14 | ENV NEXT_PUBLIC_ENV production 15 | 16 | RUN yarn build 17 | 18 | FROM node:18-alpine AS runner 19 | WORKDIR /app 20 | 21 | ENV NODE_ENV production 22 | 23 | RUN addgroup --system --gid 1001 nodejs 24 | RUN adduser --system --uid 1001 nextjs 25 | 26 | COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next 27 | COPY --from=builder /app/node_modules ./node_modules 28 | COPY --from=builder /app/package.json ./package.json 29 | 30 | USER nextjs 31 | 32 | EXPOSE 3000 33 | 34 | ENV PORT 3000 35 | 36 | CMD ["npm", "start"] 37 | -------------------------------------------------------------------------------- /packages/utils/test-videos-generator/mktestvid: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This is a really nasty hack for now. All improvements welcome. 4 | # Ideally, this file should generate quite a few different corner 5 | # cases to stress test the video ingest pipeline. 6 | 7 | # Requires ffmpeg, not libav. Example: 8 | # sudo add-apt-repository ppa:jon-severinsson/ffmpeg 9 | # sudo apt-get update 10 | # sudo apt-get install ffmpeg 11 | 12 | if [ ! -f Inconsolata.txt ]; then 13 | # Or use debian package fonts-inconsolata 14 | wget http://www.levien.com/type/myfonts/Inconsolata.otf 15 | fi 16 | 17 | ffmpeg -t 30 \ 18 | -f lavfi -i mptestsrc -y \ 19 | -vf 'scale=720:576' \ 20 | -vf 'drawtext=box=1:x=(w-text_w)/2:y=(h-text_h-line_h)/2:fontfile=./Inconsolata.otf:fontsize=48:text='"$1"''\ 21 | -target pal-dv -aspect 16:9 \ 22 | dummy_test_video.mov 23 | -------------------------------------------------------------------------------- /packages/frontend/modules/auth/forms/createRegisterForm.ts: -------------------------------------------------------------------------------- 1 | import { ObservableForm } from "modules/form/classes/ObservableForm"; 2 | import { string } from "modules/form/fields/string"; 3 | import { Manager } from "modules/state/types"; 4 | 5 | export const createRegisterForm = (manager: Manager) => { 6 | return new ObservableForm( 7 | { 8 | firstName: string().required("Du må oppgi et fornavn"), 9 | lastName: string().required("Du må oppgi et etternavn"), 10 | email: string().required("Du må oppgi en e-post addresse"), 11 | password: string() 12 | .required("Du må oppgi et passord") 13 | .min(6, "Passord må være minst 6 tegn") 14 | .max(64, "Imponerende, men ditt passord må være maksimalt 64 tegn"), 15 | }, 16 | manager 17 | ); 18 | }; 19 | 20 | export type RegisterForm = ReturnType; 21 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/hooks/useInterpolatedValue.ts: -------------------------------------------------------------------------------- 1 | import { clamp, lerp } from "modules/lang/number"; 2 | import { useRef } from "react"; 3 | import { useAnimation } from "./useAnimation"; 4 | 5 | export function useInterpolatedValue(target: number, callback: (value: number) => void, scale = 5) { 6 | const currentTimeRef = useRef(0); 7 | const valueRef = useRef(target); 8 | 9 | useAnimation(() => { 10 | const frameTime = Date.now(); 11 | const deltaTime = (frameTime - currentTimeRef.current) / 1000; 12 | currentTimeRef.current = frameTime; 13 | 14 | // a large delta value means the UI was stalled for a long time 15 | // so we don't want to animate anything if that happens 16 | if (deltaTime > 0.5) return; 17 | 18 | valueRef.current = lerp(valueRef.current, target, clamp(deltaTime * scale, 1, 0)); 19 | callback(valueRef.current); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/ButtonList.tsx: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/react"; 2 | import styled from "@emotion/styled"; 3 | 4 | /** Consistent spacing of buttons */ 5 | export const ButtonList = styled.div<{ horizontal?: boolean; compact?: boolean }>` 6 | ${(props) => { 7 | const { horizontal, compact } = props; 8 | 9 | const spacing = compact ? "12px" : "16px"; 10 | 11 | const verticalStyle = css` 12 | > * + * { 13 | margin-top: ${spacing}; 14 | } 15 | `; 16 | 17 | const horizontalStyle = css` 18 | display: flex; 19 | flex-wrap: wrap; 20 | 21 | > * { 22 | margin-right: ${spacing}; 23 | margin-bottom: ${spacing}; 24 | } 25 | 26 | margin-right: -${spacing}; 27 | margin-bottom: -${spacing}; 28 | `; 29 | 30 | return horizontal ? horizontalStyle : verticalStyle; 31 | }} 32 | `; 33 | -------------------------------------------------------------------------------- /packages/frontend/modules/lang/string.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto"; 2 | 3 | export const getHash = (bytes: number) => { 4 | return randomBytes(bytes).toString("hex"); 5 | }; 6 | 7 | export const toTitleCase = (str: string) => { 8 | const splitStr = str.toLowerCase().split(" "); 9 | 10 | const exceptions = ["i", "og", "for", "mot", "ved", "av"]; 11 | 12 | for (let i = 0; i < splitStr.length; i += 1) { 13 | if (exceptions.indexOf(splitStr[i]) === -1) { 14 | splitStr[i] = splitStr[i].charAt(0).toUpperCase() + splitStr[i].slice(1); 15 | } 16 | } 17 | 18 | return splitStr.join(" "); 19 | }; 20 | 21 | export const truncate = (str: string, length: number) => { 22 | if (str.length > length) return str.slice(0, length).trim() + "..."; 23 | return str; 24 | }; 25 | 26 | export const toSafeAsciiString = (string: string) => string.replace(/[^\x00-\x7F]/g, "").replace(/\s+/g, " "); 27 | -------------------------------------------------------------------------------- /infra/playbooks/templates/telegram-bot.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: telegram-bot-deployment 5 | namespace: monitoring 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: telegram-bot 11 | template: 12 | metadata: 13 | labels: 14 | app: telegram-bot 15 | spec: 16 | containers: 17 | - name: telegram-bot 18 | image: vientoprojects/kubernetes-monitoring-telegram-bot:latest 19 | env: 20 | - name: TELEGRAM_BOT_TOKEN 21 | valueFrom: 22 | secretKeyRef: 23 | name: telegram-config 24 | key: apiSecret 25 | --- 26 | apiVersion: v1 27 | kind: Service 28 | metadata: 29 | name: telegram-bot 30 | namespace: monitoring 31 | spec: 32 | selector: 33 | app: telegram-bot 34 | ports: 35 | - protocol: TCP 36 | port: 80 37 | targetPort: 8080 38 | -------------------------------------------------------------------------------- /packages/frontend/modules/form/helpers/checkFieldMeta.ts: -------------------------------------------------------------------------------- 1 | import { Field } from "../classes/ObservableForm"; 2 | 3 | export type Validatable = Field & { 4 | validate: () => Promise; 5 | makeDirty: () => void; 6 | touch: () => void; 7 | error?: string; 8 | touched: boolean; 9 | dirty: boolean; 10 | }; 11 | 12 | const isValidatable = (field: Field): field is Validatable => { 13 | return "validate" in field && "touch" in field && "error" && "dirty" in field; 14 | }; 15 | 16 | export const checkFieldMeta = (field: Field) => { 17 | if (isValidatable(field)) { 18 | const { error, touched, dirty, touch, makeDirty, validate } = field; 19 | return { error, touched, dirty, touch, makeDirty, validate }; 20 | } 21 | 22 | return { 23 | error: "", 24 | touched: false, 25 | dirty: false, 26 | touch: () => {}, 27 | makeDirty: () => {}, 28 | validate: async () => {}, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/frontend/modules/input/components/ControlledDropdownInput.tsx: -------------------------------------------------------------------------------- 1 | import { ObservableSelectField } from "modules/form/fields/select"; 2 | import { useField } from "modules/form/hooks/useField"; 3 | import { DropdownInput, DropdownInputProps } from "./DropdownInput"; 4 | import React from "react"; 5 | import { observer } from "mobx-react-lite"; 6 | 7 | export type ControlledDropdownInputProps = Omit & { 8 | name: string; 9 | }; 10 | 11 | export const ControlledDropdownInput = observer((props: ControlledDropdownInputProps) => { 12 | const { name, ...rest } = props; 13 | const field = useField(name); 14 | 15 | return ( 16 | field.setValue(v)} 20 | value={field.value} 21 | {...rest} 22 | /> 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/frontend/modules/schedule/resources/ScheduleItem.ts: -------------------------------------------------------------------------------- 1 | import { Resource, ResourceFactory } from "modules/state/classes/Resource"; 2 | import { Manager } from "modules/state/types"; 3 | import { VideoData } from "modules/video/types"; 4 | 5 | export type ScheduleItemData = { 6 | id: number; 7 | video: VideoData; 8 | starttime: string; 9 | endtime: string; 10 | }; 11 | 12 | export class ScheduleItem extends Resource { 13 | constructor(data: ScheduleItemData, manager: Manager) { 14 | super(data, manager); 15 | 16 | const { videoStore } = this.manager.stores; 17 | videoStore.add(data.video); 18 | } 19 | 20 | public get video() { 21 | const { videoStore } = this.manager.stores; 22 | return videoStore.getResourceById(this.data.video.id); 23 | } 24 | } 25 | 26 | export const createScheduleItem: ResourceFactory = (data, manager) => new ScheduleItem(data, manager); 27 | -------------------------------------------------------------------------------- /infra/playbooks/templates/beta/media/storage.yaml: -------------------------------------------------------------------------------- 1 | ## 2 | # This CRD defines a link between an object bucket claim and an object store. 3 | # It also defines bucket retention policy; ie. what to do when the last claim 4 | # to a bucket is rescinded. 5 | ## 6 | apiVersion: storage.k8s.io/v1 7 | kind: StorageClass 8 | metadata: 9 | name: media-buckets 10 | provisioner: rook-ceph.ceph.rook.io/bucket 11 | reclaimPolicy: Retain 12 | parameters: 13 | # The object store is defined using helm values 14 | objectStoreName: media-store 15 | objectStoreNamespace: rook-ceph 16 | --- 17 | ## 18 | # This CRD generates the "media" Secret and ConfigMap 19 | # which can be very easily attached to pods as environment 20 | # variables. 21 | ## 22 | apiVersion: objectbucket.io/v1alpha1 23 | kind: ObjectBucketClaim 24 | metadata: 25 | name: media 26 | namespace: "{{ namespace }}" 27 | spec: 28 | bucketName: media 29 | storageClassName: media-buckets -------------------------------------------------------------------------------- /infra/old/k8s/stills-uploader/003-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: Middleware 3 | metadata: 4 | name: cors 5 | namespace: default 6 | spec: 7 | headers: 8 | customResponseHeaders: 9 | Access-Control-Allow-Origin: "*" 10 | Access-Control-Allow-Headers: "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With" 11 | Access-Control-Allow-Methods: "POST" 12 | --- 13 | kind: IngressRoute 14 | apiVersion: traefik.containo.us/v1alpha1 15 | metadata: 16 | name: stills-upload 17 | namespace: default 18 | spec: 19 | entryPoints: 20 | - websecure 21 | routes: 22 | - match: Host(`stills-generator.frikanalen.no`) 23 | kind: Rule 24 | middlewares: 25 | - name: cors 26 | namespace: default 27 | services: 28 | - kind: Service 29 | name: atem-control 30 | scheme: http 31 | port: 80 32 | tls: 33 | certResolver: default 34 | -------------------------------------------------------------------------------- /packages/frontend/modules/video/components/VideoPlayer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { AspectContainer } from "modules/core/components/AspectContainer"; 3 | import React from "react"; 4 | 5 | const Container = styled.video` 6 | width: 100%; 7 | height: 100%; 8 | 9 | border-radius: 4px; 10 | overflow: hidden; 11 | 12 | height: 100%; 13 | width: 100%; 14 | 15 | box-shadow: 2px 2px 11px 2px rgba(0, 0, 0, 0.1); 16 | `; 17 | 18 | export type VideoPlayerProps = { 19 | src: string; 20 | thumbnail: string; 21 | width: number; 22 | height: number; 23 | }; 24 | 25 | export function VideoPlayer(props: VideoPlayerProps) { 26 | const { src, thumbnail, width, height } = props; 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /infra/old/k8s-beta/migrate-from-fkweb-nightly.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: migrate-from-fkweb 5 | namespace: beta 6 | spec: 7 | schedule: "00 00 * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | restartPolicy: OnFailure 13 | containers: 14 | - name: toches 15 | image: frikanalen/toches 16 | args: 17 | - sh 18 | - -c 19 | - "yarn build-cli && yarn cli migrate-fkweb" 20 | env: 21 | - name: DATABASE_URL 22 | valueFrom: 23 | secretKeyRef: 24 | name: database 25 | key: DATABASE_URL 26 | - name: FKWEB_DATABASE_URL 27 | valueFrom: 28 | secretKeyRef: 29 | name: database 30 | key: FKWEB_DATABASE_URL 31 | -------------------------------------------------------------------------------- /infra/old/k8s/stills-generator/003-ingress.yaml-probably-deprecated: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: Middleware 3 | metadata: 4 | name: stills-generator-cors 5 | spec: 6 | headers: 7 | customResponseHeaders: 8 | Access-Control-Allow-Origin: "*" 9 | Access-Control-Allow-Headers: "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With" 10 | Access-Control-Allow-Methods: "POST" 11 | --- 12 | kind: IngressRoute 13 | apiVersion: traefik.containo.us/v1alpha1 14 | metadata: 15 | name: stills-generator 16 | namespace: default 17 | spec: 18 | entryPoints: 19 | - websecure 20 | routes: 21 | - match: Host(`stills-generator.frikanalen.no`) 22 | kind: Rule 23 | middlewares: 24 | - name: stills-generator-cors 25 | services: 26 | - kind: Service 27 | name: stills-generator 28 | scheme: http 29 | port: 80 30 | tls: 31 | certResolver: default 32 | -------------------------------------------------------------------------------- /packages/frontend/modules/playout/components/ATEMControls.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonList } from "modules/ui/components/ButtonList"; 2 | import { GenericButton } from "modules/ui/components/GenericButton"; 3 | import { MixEffectsBusInput } from "../types"; 4 | 5 | export type ATEMControlsProps = { 6 | inputs: MixEffectsBusInput[]; 7 | index: number; 8 | onChange: (index: number) => void; 9 | }; 10 | 11 | export function ATEMControls(props: ATEMControlsProps) { 12 | const { inputs, index, onChange } = props; 13 | 14 | return ( 15 | 16 | {inputs.map((input) => ( 17 | onChange(input.index)} 23 | /> 24 | ))} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "useDefineForClassFields": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "baseUrl": ".", 22 | "noUnusedParameters": false, // So it won't complain about getstatic... etc 23 | "noUnusedLocals": false, // So it won't complain about getstatic... etc 24 | "incremental": true 25 | }, 26 | "include": [ 27 | "types/*.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/utils/stills-generator/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask import Response 3 | from flask import request 4 | from chargen import Poster 5 | app = Flask(__name__) 6 | 7 | 8 | def makePoster(): 9 | if request.method == 'POST': 10 | print(request.is_json) 11 | args = request.get_json() 12 | print(args) 13 | heading = args['heading'] 14 | text = args['text'] 15 | else: 16 | heading = request.args.get('heading') 17 | text = request.args.get('text') 18 | 19 | return Poster({'heading': heading, 'text': text}) 20 | 21 | @app.route('/getPoster.rgba', methods=['GET', 'POST']) 22 | def getRGBA(): 23 | poster = makePoster() 24 | return Response(poster.getRGBA(), mimetype='image/x-rgb') 25 | 26 | @app.route('/getPoster.png', methods=['GET', 'POST']) 27 | def getPNG(): 28 | poster = makePoster() 29 | return Response(poster.getPNG(), mimetype='image/png') 30 | -------------------------------------------------------------------------------- /packages/frontend/modules/organization/resources/Organization.ts: -------------------------------------------------------------------------------- 1 | import { Resource, ResourceFactory } from "modules/state/classes/Resource"; 2 | 3 | export type OrganizationData = { 4 | id: number; 5 | name: string; 6 | description: string; 7 | postalAddress: string; 8 | streetAddress: string; 9 | 10 | // Temporary 11 | editorId: number; 12 | editorName: string; 13 | editorEmail: string; 14 | editorMsisdn: string; 15 | }; 16 | 17 | export class Organization extends Resource { 18 | public get videos() { 19 | const { id } = this.data; 20 | const { listStore } = this.manager.stores; 21 | 22 | return listStore.ensure(`videos-organization-${id}`, "video", { 23 | path: "/videos", 24 | params: { 25 | organization: id, 26 | }, 27 | }); 28 | } 29 | } 30 | 31 | export const createOrganization: ResourceFactory = (data, manager) => new Organization(data, manager); 32 | -------------------------------------------------------------------------------- /infra/old/k8s/playout/playout-primary.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: playout 5 | spec: 6 | serviceName: playout 7 | selector: 8 | matchLabels: 9 | app: playout 10 | template: 11 | metadata: 12 | annotations: 13 | co.elastic.logs/json.add_error_key: "true" 14 | co.elastic.logs/json.overwrite_keys: "true" 15 | co.elastic.logs/json.keys_under_root: "true" 16 | labels: 17 | app: playout 18 | spec: 19 | containers: 20 | - name: playout 21 | image: frikanalen/playout:latest 22 | env: 23 | - name: CASPAR_HOST 24 | value: tx3 25 | imagePullPolicy: Always 26 | volumeMounts: 27 | - name: tz-config 28 | mountPath: /etc/localtime 29 | volumes: 30 | - name: tz-config 31 | hostPath: 32 | path: /usr/share/zoneinfo/Europe/Oslo 33 | type: File 34 | -------------------------------------------------------------------------------- /infra/old/k8s/monitoring/junos_exporter/README.md: -------------------------------------------------------------------------------- 1 | ## Monitoring of switches 2 | 3 | Until we get fksw1 online, only fksw2 is monitored. 4 | 5 | Junos configuration: 6 | 7 | ``` 8 | login { 9 | user junos_exporter { 10 | uid 1337; 11 | class read-only; 12 | authentication { 13 | ssh-rsa "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDa16/bpm7QLtsjaGfSIwyMaD9ArJvjWjfijrf+uGtgB5KpugkUFUoaEqcKsdU+mLAMi/CWG/U3toA19jSwT62gf3mIfayyvNss34YylUUKUtfrOS/iRNB3KnfYyxr2g4e0xbmpI60C3LUmATNZUBhl86KtrjH96+i3G+8NOmFcO1dzxix74mNOQKWtBI8PscD/4EtJWL3h4sYJnDAwqMeiMhKkv0mAhx0B01+vrOG40/oP4R5OtkVaG0bDErEFxGPB1tcpjJLCs6/27sOQgS/2ecybgAFqiE65klLAO4jhSP9iz4mNr82/9eBLFoLWIdvT8EJM8TnpgmhGrR01t6H8hbjnj6AwhVzW3TV2TVXeAP0Wxkx2ttMYwRFXBgSk+QD/0La5om1ZzQWEYGH30VOHukKG8ejTWl5KIYpCyPalvMCfIvVRV98lLt9tT3NjFDR/PYBp4ZGM5b2JEefCJqndsPTlGTnhrtbuqVINO8PEx+ydj+Q7O2+AXsMiCdJACW8= toresbe@simula"; ## SECRET-DATA 14 | } 15 | } 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /infra/old/k8s-legacy/ingress-graphics.yaml: -------------------------------------------------------------------------------- 1 | kind: IngressRoute 2 | apiVersion: traefik.containo.us/v1alpha1 3 | metadata: 4 | name: scheme-redirect-graphics 5 | namespace: default 6 | spec: 7 | entryPoints: 8 | - web 9 | routes: 10 | - match: Host(`www.frikanalen.no`) || Host(`frikanalen.no`) && PathPrefix(`/graphics`) 11 | priority: 100 12 | kind: Rule 13 | services: 14 | - kind: Service 15 | name: graphics 16 | scheme: http 17 | port: 80 18 | --- 19 | kind: IngressRoute 20 | apiVersion: traefik.containo.us/v1alpha1 21 | metadata: 22 | name: graphics 23 | namespace: default 24 | spec: 25 | entryPoints: 26 | - websecure 27 | routes: 28 | - match: Host(`www.frikanalen.no`) || Host(`frikanalen.no`) && PathPrefix(`/graphics`) 29 | priority: 100 30 | kind: Rule 31 | services: 32 | - kind: Service 33 | name: graphics 34 | scheme: http 35 | port: 80 36 | tls: 37 | certResolver: default 38 | -------------------------------------------------------------------------------- /infra/old/k8s/streaming/001-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | #in=udp://0.0.0.0:9001,stream=video,bw=800000,init_segment=livehd-video1.mp4,template=livehd-video1-$Number$.mp4 3 | #in=udp://0.0.0.0:9002,stream=video,bw=1400000,init_segment=livehd-video2.mp4,template=livehd-video2-$Number$.mp4 4 | kind: ConfigMap 5 | metadata: 6 | name: shaka-config 7 | namespace: streaming 8 | data: 9 | arguments: >- 10 | in=udp://0.0.0.0:9000,stream=audio,init_segment=livehd-audio.mp4,segment_template=livehd-audio-$Number$.mp4 11 | in=udp://0.0.0.0:9002,stream=video,bw=1400000,init_segment=livehd-video2.mp4,template=livehd-video2-$Number$.mp4 12 | in=udp://0.0.0.0:9003,stream=video,bw=3000000,init_segment=livehd-video3.mp4,template=livehd-video3-$Number$.mp4 13 | --mpd_output index.mpd --dump_stream_info --min_buffer_time=10 14 | --hls_playlist_type LIVE --hls_master_playlist_output index.m3u8 15 | --time_shift_buffer_depth=300 --segment_duration=3 --io_block_size 65536 16 | 17 | -------------------------------------------------------------------------------- /packages/utils/atem-control/src/atem/AtemInterface.ts: -------------------------------------------------------------------------------- 1 | import { RealAtem } from "./RealAtem.js"; 2 | import { MockAtem } from "./MockAtem.js"; 3 | import type { Atem } from "atem-connection"; 4 | import { getLogger } from "../logger.js"; 5 | 6 | const logger = getLogger(); 7 | 8 | export interface AtemMixEffects { 9 | readonly program: number; 10 | readonly preview: number; 11 | 12 | setProgram: (inputIndex: number) => Promise; 13 | setPreview: (inputIndex: number) => Promise; 14 | } 15 | 16 | export interface AtemConnection { 17 | ME: AtemMixEffects[]; 18 | connect: (atemHost: string) => Promise; 19 | atem: Atem | undefined; 20 | } 21 | 22 | const getAtem = (): AtemConnection => { 23 | if (process.env["ATEM_HOST"]) { 24 | return new RealAtem(); 25 | } else { 26 | logger.warn("ATEM_HOST environment not set; operating in dummy mode!"); 27 | return new MockAtem(); 28 | } 29 | }; 30 | 31 | const atem = getAtem(); 32 | export default atem; 33 | -------------------------------------------------------------------------------- /infra/old/k8s/multiviewer-streaming/001-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | #in=udp://0.0.0.0:9001,stream=video,bw=800000,init_segment=livehd-video1.mp4,template=livehd-video1-$Number$.mp4 3 | #in=udp://0.0.0.0:9002,stream=video,bw=1400000,init_segment=livehd-video2.mp4,template=livehd-video2-$Number$.mp4 4 | kind: ConfigMap 5 | metadata: 6 | name: shaka-config 7 | namespace: streaming 8 | data: 9 | arguments: >- 10 | in=udp://0.0.0.0:9000,stream=audio,init_segment=livehd-audio.mp4,segment_template=livehd-audio-$Number$.mp4 11 | in=udp://0.0.0.0:9002,stream=video,bw=1400000,init_segment=livehd-video2.mp4,template=livehd-video2-$Number$.mp4 12 | in=udp://0.0.0.0:9003,stream=video,bw=3000000,init_segment=livehd-video3.mp4,template=livehd-video3-$Number$.mp4 13 | --mpd_output index.mpd --dump_stream_info --min_buffer_time=10 14 | --hls_playlist_type LIVE --hls_master_playlist_output index.m3u8 15 | --time_shift_buffer_depth=300 --segment_duration=3 --io_block_size 65536 16 | 17 | -------------------------------------------------------------------------------- /packages/frontend/modules/input/components/ControlledTextInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextInput, TextInputProps } from "./TextInput"; 3 | import { useField } from "../../form/hooks/useField"; 4 | import { ObservableStringField } from "../../form/fields/string"; 5 | import { observer } from "mobx-react-lite"; 6 | 7 | export type ControlledTextInputProps = Omit & { 8 | type?: "text" | "password" | "number"; 9 | name: string; 10 | }; 11 | 12 | export const ControlledTextInput = observer((props: ControlledTextInputProps) => { 13 | const { name, ...rest } = props; 14 | 15 | const field = useField(name); 16 | 17 | const inputProps = { 18 | value: field.value, 19 | invalid: !!(field.error && field.touched), 20 | }; 21 | 22 | return ( 23 | field.setValue(e.target.value)} 25 | onBlur={() => field.touch()} 26 | {...inputProps} 27 | {...rest} 28 | /> 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /infra/old/k8s/playout/schedule-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: schedule-service 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: schedule-service 9 | minReadySeconds: 5 10 | template: 11 | metadata: 12 | labels: 13 | app: schedule-service 14 | spec: 15 | containers: 16 | - name: schedule-service 17 | env: 18 | - name: DATABASE_URL 19 | valueFrom: 20 | secretKeyRef: 21 | name: database-api-secret 22 | key: DATABASE_URL 23 | - name: TZ 24 | value: Europe/Oslo 25 | image: frikanalen/schedule-service:latest 26 | imagePullPolicy: Always 27 | --- 28 | kind: Service 29 | apiVersion: v1 30 | metadata: 31 | name: schedule-service 32 | spec: 33 | type: ClusterIP 34 | selector: 35 | app: schedule-service 36 | ports: 37 | - protocol: TCP 38 | port: 80 39 | targetPort: 80 40 | name: web 41 | -------------------------------------------------------------------------------- /infra/playbooks/rook-ceph.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | tasks: 3 | - name: Add rook-ceph Helm repo 4 | kubernetes.core.helm_repository: 5 | name: rook-release 6 | repo_url: "https://charts.rook.io/release" 7 | - name: Deploy rook-ceph 8 | kubernetes.core.helm: 9 | name: rook-ceph 10 | chart_ref: rook-release/rook-ceph 11 | release_namespace: rook-ceph 12 | create_namespace: true 13 | values: "{{ lookup('ansible.builtin.file', 'rook-ceph-values.yaml') | from_yaml }}" 14 | - name: Deploy rook-ceph cluster 15 | kubernetes.core.helm: 16 | name: rook-ceph-cluster 17 | chart_ref: rook-release/rook-ceph-cluster 18 | release_namespace: rook-ceph 19 | create_namespace: true 20 | values: "{{ lookup('ansible.builtin.file', 'rook-ceph-cluster-values.yaml') | from_yaml }}" 21 | - name: Deploy rook-ceph monitoring 22 | community.kubernetes.k8s: 23 | state: present 24 | src: files/rook-ceph-monitoring.yaml 25 | -------------------------------------------------------------------------------- /packages/frontend/modules/styling/components/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "@emotion/react"; 2 | import { useCookie } from "modules/state/hooks/useCookie"; 3 | import React from "react"; 4 | import { darkTheme, lightTheme } from "../themes"; 5 | 6 | export type ThemeType = "light" | "dark"; 7 | 8 | // NAMING THINGS IS HARD 9 | export type ThemeContextContext = { 10 | type: ThemeType; 11 | toggle: () => void; 12 | }; 13 | 14 | export const context = React.createContext({ 15 | type: "light", 16 | toggle: () => {}, 17 | }); 18 | 19 | export function ThemeContext(props: React.PropsWithChildren<{}>) { 20 | const { children } = props; 21 | const [type, setType] = useCookie("theme", "light"); 22 | 23 | return ( 24 | 25 | setType(type === "light" ? "dark" : "light") }}> 26 | {children} 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /infra/old/k8s-legacy/tmp-acme-pv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | labels: 5 | type: local 6 | name: tmp-acme-pv 7 | spec: 8 | accessModes: 9 | - ReadWriteOnce 10 | capacity: 11 | storage: 1Gi 12 | local: 13 | path: /pv/db 14 | nodeAffinity: 15 | required: 16 | nodeSelectorTerms: 17 | - matchExpressions: 18 | - key: kubernetes.io/hostname 19 | operator: In 20 | values: 21 | - tx1 22 | persistentVolumeReclaimPolicy: Retain 23 | storageClassName: local-storage 24 | volumeMode: Filesystem 25 | --- 26 | apiVersion: v1 27 | kind: PersistentVolumeClaim 28 | metadata: 29 | name: tmp-acme-pvc 30 | namespace: default 31 | spec: 32 | accessModes: 33 | - ReadWriteOnce 34 | resources: 35 | requests: 36 | storage: 1G 37 | storageClassName: local-storage 38 | volumeMode: Filesystem 39 | volumeName: tmp-acme-pv 40 | status: 41 | accessModes: 42 | - ReadWriteOnce 43 | capacity: 44 | storage: 1Gi 45 | phase: Bound 46 | -------------------------------------------------------------------------------- /packages/frontend/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | # - airbnb-typescript 6 | - "plugin:react/recommended" 7 | - "prettier" 8 | - "next" 9 | parserOptions: 10 | project: "./tsconfig.json" 11 | ecmaFeatures: 12 | jsx: true 13 | ecmaVersion: 12 14 | sourceType: module 15 | failOnError: false 16 | emitWarning: true 17 | plugins: 18 | - react 19 | - "@typescript-eslint" 20 | rules: 21 | react/static-property-placement: 22 | - "warn" 23 | - "property assignment" 24 | - contextType: "static public field" 25 | contextTypes: "static public field" 26 | no-use-before-define: "off" 27 | "react/display-name": "off" 28 | "@typescript-eslint/no-unused-vars": ["warn"] 29 | no-unused-vars: "off" 30 | react/prop-types: "off" 31 | "react/jsx-filename-extension": 32 | - 2 33 | - "extensions": 34 | - ".jsx" 35 | - ".tsx" 36 | overrides: 37 | - files: 38 | - "*.ts" 39 | - "*.tsx" 40 | ignorePatterns: 41 | - "*.js" 42 | - "*.jsx" 43 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/Quote.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { PropsWithChildren } from "react"; 3 | import { ExternalLink } from "./ExternalLink"; 4 | 5 | const Container = styled.figure` 6 | margin: 24px 0px; 7 | `; 8 | 9 | const Block = styled.blockquote` 10 | font-size: 1.2em; 11 | font-style: italic; 12 | 13 | color: ${(props) => props.theme.fontColor.muted}; 14 | `; 15 | 16 | const Caption = styled.figcaption` 17 | margin-top: 8px; 18 | `; 19 | 20 | export type QuoteProps = PropsWithChildren<{ 21 | className?: string; 22 | citation: { 23 | name: string; 24 | href: string; 25 | }; 26 | }>; 27 | 28 | export function Quote(props: QuoteProps) { 29 | const { className, children, citation } = props; 30 | 31 | return ( 32 | 33 | «{children}» 34 | 35 | — {citation.name} 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/frontend/modules/video/forms/createNewVideoForm.ts: -------------------------------------------------------------------------------- 1 | import { ObservableForm } from "modules/form/classes/ObservableForm"; 2 | import { Option, select } from "modules/form/fields/select"; 3 | import { string } from "modules/form/fields/string"; 4 | import { Manager } from "modules/state/types"; 5 | import { VideoCategoryData } from "../types"; 6 | 7 | export const createNewVideoForm = (categories: VideoCategoryData[], manager: Manager) => { 8 | const categoryOptions: Option[] = categories.map((c) => ({ 9 | // FIXME: API expects a string (name) rather than an id 10 | value: c.name, 11 | label: c.name, 12 | })); 13 | 14 | return new ObservableForm( 15 | { 16 | name: string().required(), 17 | header: string(), 18 | description: string(), 19 | categories: select({ 20 | options: categoryOptions, 21 | multiple: true, 22 | value: [], 23 | }).required(), 24 | }, 25 | manager 26 | ); 27 | }; 28 | 29 | export type NewVideoForm = ReturnType; 30 | -------------------------------------------------------------------------------- /packages/frontend/modules/video/components/VideoGrid.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { EmptyState } from "modules/ui/components/EmptyState"; 3 | import { Video } from "../resources/Video"; 4 | import { VideoGridItem } from "./VideoGridItem"; 5 | 6 | const Container = styled.div``; 7 | 8 | const Grid = styled.ul` 9 | display: grid; 10 | grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); 11 | gap: 16px; 12 | `; 13 | 14 | export type VideoGridProps = { 15 | videos: Video[]; 16 | }; 17 | 18 | export function VideoGrid(props: VideoGridProps) { 19 | const { videos } = props; 20 | 21 | const renderEmptyState = () => { 22 | if (videos.length > 0) return null; 23 | 24 | return ; 25 | }; 26 | 27 | return ( 28 | 29 | {renderEmptyState()} 30 | 31 | {videos.map((v) => ( 32 | 33 | ))} 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /infra/old/k8s-dev/s3-emulator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: s3-ninja 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: s3-ninja 10 | minReadySeconds: 5 11 | template: 12 | metadata: 13 | annotations: 14 | linkerd.io/inject: enabled 15 | labels: 16 | app: s3-ninja 17 | spec: 18 | containers: 19 | - name: s3-ninja 20 | image: scireum/s3-ninja:latest 21 | ports: 22 | - name: web 23 | containerPort: 9000 24 | --- 25 | kind: Service 26 | apiVersion: v1 27 | metadata: 28 | name: s3-ninja 29 | spec: 30 | type: ClusterIP 31 | selector: 32 | app: s3-ninja 33 | ports: 34 | - protocol: TCP 35 | port: 9000 36 | name: web 37 | --- 38 | kind: Service 39 | apiVersion: v1 40 | metadata: 41 | name: s3-backend 42 | spec: 43 | type: ClusterIP 44 | selector: 45 | app: s3-ninja 46 | ports: 47 | - protocol: TCP 48 | port: 80 49 | name: web 50 | targetPort: 9000 51 | -------------------------------------------------------------------------------- /infra/old/k8s-legacy/tmp-db-pv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | labels: 5 | type: local 6 | name: tmp-db-pv 7 | spec: 8 | accessModes: 9 | - ReadWriteOnce 10 | capacity: 11 | storage: 1Gi 12 | local: 13 | path: /pv/db 14 | nodeAffinity: 15 | required: 16 | nodeSelectorTerms: 17 | - matchExpressions: 18 | - key: kubernetes.io/hostname 19 | operator: In 20 | values: 21 | - tx1 22 | persistentVolumeReclaimPolicy: Retain 23 | storageClassName: local-storage 24 | volumeMode: Filesystem 25 | --- 26 | apiVersion: v1 27 | kind: PersistentVolumeClaim 28 | metadata: 29 | name: postgres-db-database-api-1-0 30 | namespace: default 31 | spec: 32 | accessModes: 33 | - ReadWriteOnce 34 | resources: 35 | requests: 36 | storage: 1G 37 | storageClassName: local-storage 38 | volumeMode: Filesystem 39 | volumeName: tmp-db-pv 40 | status: 41 | accessModes: 42 | - ReadWriteOnce 43 | capacity: 44 | storage: 1Gi 45 | phase: Bound 46 | -------------------------------------------------------------------------------- /packages/utils/atem-control/src/atem/MockAtem.ts: -------------------------------------------------------------------------------- 1 | import type { AtemConnection, AtemMixEffects } from "./AtemInterface.js"; 2 | 3 | class MockAtemME implements AtemMixEffects { 4 | idx: number; 5 | currentProgram: number; 6 | currentPreview: number; 7 | 8 | constructor(idx: number) { 9 | this.idx = idx; 10 | this.currentProgram = 1; 11 | this.currentPreview = 1; 12 | } 13 | 14 | public get program() { 15 | return this.currentProgram; 16 | } 17 | 18 | public get preview() { 19 | return this.currentProgram; 20 | } 21 | 22 | public setProgram = async (inputIndex: number) => { 23 | this.currentProgram = inputIndex; 24 | }; 25 | 26 | public setPreview = async (inputIndex: number) => { 27 | this.currentPreview = inputIndex; 28 | }; 29 | } 30 | 31 | export class MockAtem implements AtemConnection { 32 | public ME: AtemMixEffects[]; 33 | atem: undefined; 34 | 35 | constructor() { 36 | this.ME = [new MockAtemME(0)]; 37 | } 38 | 39 | async connect(_hostName: string) { 40 | return; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /infra/old/k8s/playout/playout-secondary.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: playout-secondary 5 | spec: 6 | serviceName: playout-secondary 7 | selector: 8 | matchLabels: 9 | app: playout-secondary 10 | template: 11 | metadata: 12 | annotations: 13 | co.elastic.logs/json.add_error_key: "true" 14 | co.elastic.logs/json.overwrite_keys: "true" 15 | co.elastic.logs/json.keys_under_root: "true" 16 | labels: 17 | app: playout-secondary 18 | spec: 19 | containers: 20 | - name: playout-secondary 21 | image: frikanalen/playout:latest 22 | env: 23 | - name: USE_ORIGINAL 24 | value: "yes" 25 | - name: CASPAR_HOST 26 | value: tx2 27 | imagePullPolicy: Always 28 | volumeMounts: 29 | - name: tz-config 30 | mountPath: /etc/localtime 31 | volumes: 32 | - name: tz-config 33 | hostPath: 34 | path: /usr/share/zoneinfo/Europe/Oslo 35 | type: File 36 | -------------------------------------------------------------------------------- /packages/frontend/modules/form/components/FormField.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from "react"; 2 | import { FieldError } from "./FieldError"; 3 | import styled from "@emotion/styled"; 4 | 5 | export type FormFieldProps = PropsWithChildren<{ 6 | className?: string; 7 | name: string; 8 | label: string; 9 | }>; 10 | 11 | const Container = styled.div` 12 | display: flex; 13 | flex-direction: column; 14 | `; 15 | 16 | const Label = styled.label` 17 | font-weight: 600; 18 | font-size: 1em; 19 | letter-spacing: 0.025em; 20 | 21 | margin-bottom: 12px; 22 | color: ${(props) => props.theme.fontColor.normal}; 23 | `; 24 | 25 | export function FormField(props: FormFieldProps) { 26 | const { children, name, label, className } = props; 27 | 28 | return ( 29 | 30 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | 37 | export type FormFieldWithProps = (props: FormFieldProps & T) => JSX.Element; 38 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { css, keyframes } from "@emotion/react"; 2 | import styled from "@emotion/styled"; 3 | 4 | const spin = keyframes` 5 | to { 6 | transform: rotate(350deg); 7 | } 8 | `; 9 | 10 | const Container = styled.div<{ size: Size }>` 11 | ${(props) => { 12 | const size = props.size === "small" ? 24 : 48; 13 | 14 | return css` 15 | width: ${size}px; 16 | height: ${size}px; 17 | `; 18 | }} 19 | 20 | border: solid ${(props) => props.theme.color.divider}; 21 | border-width: ${(props) => (props.size === "small" ? 3 : 4)}px; 22 | border-top-color: ${(props) => props.theme.color.accent}; 23 | 24 | border-radius: 100%; 25 | animation: ${spin} infinite 800ms linear; 26 | `; 27 | 28 | type Size = "small" | "normal"; 29 | 30 | export type SpinnerProps = { 31 | className?: string; 32 | size: Size; 33 | }; 34 | 35 | export function Spinner(props: SpinnerProps) { 36 | const { className, size } = props; 37 | 38 | return ; 39 | } 40 | -------------------------------------------------------------------------------- /infra/old/charts/frontend/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "frontend.fullname" . }} 6 | labels: 7 | {{- include "frontend.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "frontend.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /infra/old/k8s/graphics/graphics.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: graphics 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: graphics 10 | minReadySeconds: 5 11 | template: 12 | metadata: 13 | annotations: 14 | linkerd.io/inject: enabled 15 | labels: 16 | app: graphics 17 | spec: 18 | containers: 19 | - name: graphics 20 | image: frikanalen/graphics:latest 21 | imagePullPolicy: Always 22 | ports: 23 | - name: web 24 | containerPort: 80 25 | readinessProbe: 26 | httpGet: 27 | path: / 28 | port: 80 29 | initialDelaySeconds: 15 30 | livenessProbe: 31 | httpGet: 32 | path: / 33 | port: 80 34 | --- 35 | kind: Service 36 | apiVersion: v1 37 | metadata: 38 | name: graphics 39 | spec: 40 | type: ClusterIP 41 | selector: 42 | app: graphics 43 | ports: 44 | - protocol: TCP 45 | port: 80 46 | name: web 47 | -------------------------------------------------------------------------------- /infra/old/k8s/ingress/old/003-service.yaml: -------------------------------------------------------------------------------- 1 | # ServiceAccount 2 | --- 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | name: traefik 7 | namespace: kube-system 8 | 9 | # Service 10 | --- 11 | apiVersion: v1 12 | kind: Service 13 | metadata: 14 | labels: 15 | app: traefik 16 | release: traefik 17 | name: traefik 18 | namespace: kube-system 19 | spec: 20 | externalIPs: 21 | - 192.168.3.100 22 | externalTrafficPolicy: Cluster 23 | ports: 24 | - name: web 25 | nodePort: 31909 26 | port: 80 27 | protocol: TCP 28 | targetPort: 80 29 | - name: synapse 30 | nodePort: 30931 31 | port: 8448 32 | protocol: TCP 33 | targetPort: 8448 34 | - name: websecure 35 | nodePort: 30584 36 | port: 443 37 | protocol: TCP 38 | targetPort: 443 39 | - name: admin 40 | nodePort: 32316 41 | port: 8100 42 | protocol: TCP 43 | targetPort: 8100 44 | selector: 45 | app: traefik 46 | release: traefik 47 | sessionAffinity: None 48 | type: LoadBalancer 49 | status: 50 | loadBalancer: {} 51 | -------------------------------------------------------------------------------- /infra/old/roles/upload-app/templates/apache.conf.j2: -------------------------------------------------------------------------------- 1 | 2 | ServerName upload.frikanalen.no 3 | ServerAlias ftp.frikanalen.no 4 | ServerAlias ftp.frikanalen.tv 5 | ErrorLog {{app_dir}}/logs/error.log 6 | CustomLog {{app_dir}}/logs/access.log combined 7 | 8 | ProxyPreserveHost On 9 | ProxyPass / http://127.0.0.1:3000/ 10 | ProxyPassReverse / http://127.0.0.1:3000/ 11 | 12 | 13 | 14 | ServerName upload.frikanalen.no 15 | ServerAlias ftp.frikanalen.no 16 | ServerAlias ftp.frikanalen.tv 17 | ErrorLog {{app_dir}}/logs/error.log 18 | CustomLog {{app_dir}}/logs/access.log combined 19 | 20 | ProxyPreserveHost On 21 | ProxyPass / http://127.0.0.1:3000/ 22 | ProxyPassReverse / http://127.0.0.1:3000/ 23 | 24 | SSLEngine on 25 | SSLCertificateFile /etc/letsencrypt/live/upload.frikanalen.no/cert.pem 26 | SSLCertificateKeyFile /etc/letsencrypt/live/upload.frikanalen.no/privkey.pem 27 | SSLCertificateChainFile /etc/letsencrypt/live/upload.frikanalen.no/chain.pem 28 | 29 | -------------------------------------------------------------------------------- /infra/old/k8s/ingress/kiloview.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: kiloview 5 | namespace: default 6 | spec: 7 | type: ExternalName 8 | externalName: 192.168.3.168 9 | --- 10 | kind: Service 11 | apiVersion: v1 12 | metadata: 13 | name: kiloview-stream 14 | namespace: default 15 | spec: 16 | type: ExternalName 17 | externalName: 192.168.3.167 18 | --- 19 | kind: IngressRoute 20 | apiVersion: traefik.containo.us/v1alpha1 21 | metadata: 22 | name: kiloview 23 | namespace: default 24 | spec: 25 | entryPoints: 26 | - websecure 27 | routes: 28 | - match: Host(`kiloview.frikanalen.no`) 29 | kind: Rule 30 | services: 31 | - kind: Service 32 | name: kiloview 33 | scheme: http 34 | port: 80 35 | tls: 36 | certResolver: default 37 | --- 38 | apiVersion: traefik.containo.us/v1alpha1 39 | kind: IngressRouteTCP 40 | metadata: 41 | name: kiloview-rtmp 42 | namespace: default 43 | spec: 44 | entryPoints: 45 | - rtmp 46 | routes: 47 | - match: HostSNI(`*`) 48 | services: 49 | - name: kiloview-stream 50 | port: 1935 51 | -------------------------------------------------------------------------------- /infra/old/k8s-beta/schedule.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: Middleware 3 | metadata: 4 | name: strip-www 5 | namespace: beta 6 | spec: 7 | redirectRegex: 8 | permanent: true 9 | regex: ^https://www.frikanalen.no/(.*) 10 | replacement: https://frikanalen.no/${1} 11 | --- 12 | apiVersion: traefik.containo.us/v1alpha1 13 | kind: Middleware 14 | metadata: 15 | name: epgxml-prefix 16 | namespace: beta 17 | spec: 18 | addPrefix: 19 | prefix: /epg 20 | --- 21 | kind: IngressRoute 22 | apiVersion: traefik.containo.us/v1alpha1 23 | metadata: 24 | name: toches-xmltv 25 | namespace: beta 26 | spec: 27 | entryPoints: 28 | - websecure 29 | routes: 30 | - match: (Host(`frikanalen.no`) || Host(`www.frikanalen.no`)) && PathPrefix(`/xmltv`) 31 | priority: 1000 32 | kind: Rule 33 | middlewares: 34 | - name: strip-www 35 | namespace: beta 36 | - name: epgxml-prefix 37 | namespace: beta 38 | services: 39 | - kind: Service 40 | name: toches 41 | scheme: http 42 | port: 80 43 | tls: 44 | certResolver: default 45 | -------------------------------------------------------------------------------- /infra/playbooks/README.md: -------------------------------------------------------------------------------- 1 | In this directory is Ansible to manage Helm and Kubernetes deployments. 2 | 3 | 4 | ## Requirements 5 | 6 | ```bash 7 | ansible-galaxy install -r requirements.yml 8 | pip3 install -r requirements.txt 9 | # -- or -- 10 | sudo apt install python3-kubernetes python3-openshift 11 | ``` 12 | 13 | 14 | ## Vault 15 | 16 | We store our keys in an encrypted vault. To create your own, do this: 17 | 18 | ```bash 19 | ansible-vault encrypt default-secrets.yml --output vault.yml 20 | ``` 21 | 22 | ## Grafana dashboards 23 | 24 | Grafana dashboards are read from configmaps with the `grafana_dashboard` label. 25 | 26 | Example: 27 | ```bash 28 | wget https://grafana.com/api/dashboards/12586/revisions/1/download -O zfs_configmap.json 29 | # Workaround for https://github.com/grafana/grafana/issues/10786 30 | sed -i 's/${DS_PROMETHEUS}/Prometheus/g' 31 | kubectl -n monitoring create configmap grafana-dashboards --from-file=zfs_configmap.json 32 | kubectl -n monitoring label cm grafana-dashboards grafana_dashboard="1" 33 | ``` 34 | 35 | 36 | ## Usage 37 | 38 | ```bash 39 | ansible-playbook site.yml 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { IconType } from "../types"; 3 | import { SVGIcon } from "./SVGIcon"; 4 | 5 | const Container = styled.div` 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | margin: 64px 0px; 10 | `; 11 | 12 | const Title = styled.h1` 13 | font-size: 1.4em; 14 | font-weight: 600; 15 | `; 16 | 17 | const Subtitle = styled.h2` 18 | font-size: 1.1em; 19 | font-weight: 500; 20 | 21 | margin-top: 8px; 22 | `; 23 | 24 | const Icon = styled(SVGIcon)` 25 | color: ${(props) => props.theme.fontColor.subdued}; 26 | width: 64px; 27 | height: 64px; 28 | 29 | margin-bottom: 32px; 30 | `; 31 | 32 | export type EmptyStateProps = { 33 | icon: IconType; 34 | title: string; 35 | subtitle?: string; 36 | }; 37 | 38 | export function EmptyState(props: EmptyStateProps) { 39 | const { icon, title, subtitle } = props; 40 | 41 | return ( 42 | 43 | 44 | {title} 45 | {subtitle} 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /packages/utils/atem-control/src/auth.ts: -------------------------------------------------------------------------------- 1 | import { getLogger } from "./logger.js"; 2 | import { V1CheckStaff } from "./auth/sessionV1.js"; 3 | import { V2CheckStaff } from "./auth/sessionV2.js"; 4 | import { FetchError } from "node-fetch"; 5 | 6 | const logger = getLogger(); 7 | 8 | type authenticationCookies = { 9 | sessionid?: string; // API v1 session ID 10 | "fk-session"?: string; // API v2 session ID 11 | }; 12 | 13 | export const checkIfStaff = async (requestCookies: authenticationCookies) => { 14 | const V1SessionId = requestCookies?.["sessionid"]; 15 | const V2SessionId = requestCookies?.["fk-session"]; 16 | try { 17 | if (V2SessionId) { 18 | return V2CheckStaff(V2SessionId); 19 | } else if (V1SessionId) { 20 | return V1CheckStaff(V1SessionId); 21 | } 22 | } catch (e: any) { 23 | if (e instanceof FetchError) { 24 | console.error(`HTTP ${e.code}, ${e.message}`); 25 | } else { 26 | console.error(`Error: ${e.toString()}`); 27 | } 28 | 29 | return false; 30 | } 31 | logger.warn("Rejecting request with neither v1 nor v2 API session cookie"); 32 | return false; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/utils/atem-control/src/auth/sessionV1.ts: -------------------------------------------------------------------------------- 1 | import { FK_API_URL } from "../server.js"; 2 | import { getLogger } from "../logger.js"; 3 | const logger = getLogger(); 4 | import { z } from "zod"; 5 | import fetch from "node-fetch"; 6 | 7 | const V1ProfileResponseSchema = z.object({ 8 | id: z.number(), 9 | email: z.string(), 10 | isStaff: z.boolean(), 11 | }); 12 | 13 | type V1ProfileResponse = z.infer; 14 | 15 | export const getV1Profile = async ( 16 | sessionId: string 17 | ): Promise => { 18 | const res = await fetch(`${FK_API_URL}/user`, { 19 | headers: { 20 | cookie: `sessionid=${sessionId}`, 21 | }, 22 | }); 23 | return V1ProfileResponseSchema.parse(await res.json()); 24 | }; 25 | 26 | export const V1CheckStaff = async (sessionId: string) => { 27 | const userProfile = await getV1Profile(sessionId); 28 | 29 | if (userProfile.isStaff) { 30 | logger.info(`Authenticated API V1 request for ${userProfile.email}`); 31 | return true; 32 | } 33 | 34 | logger.info(`User ${userProfile.email} is not staff, refusing`); 35 | return false; 36 | }; 37 | -------------------------------------------------------------------------------- /infra/old/roles/playout/templates/update.githook.j2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | refname="$1" 5 | oldrev="$2" 6 | newrev="$3" 7 | 8 | echo $1 $2 $3 9 | 10 | if [ -z "$GIT_DIR" ]; then echo GIT_DIR missing; exit 1; fi 11 | if [ -z "$refname" ]; then echo params missing; exit 1; fi 12 | 13 | echo "> Deploying app" 14 | dir={{app_dir}}/app 15 | #preserve cache and logs 16 | mv $dir/cache {{app_dir}} 17 | TMPDIR=$(mktemp -d -p {{app_dir}} deploy_XXXX) 18 | git --work-tree="$TMPDIR" checkout -qf "$newrev" -- packages/playout 19 | rm -r {{app_dir}}/app/* 20 | cp -ra $TMPDIR/packages/playout/* $dir 21 | rm -r {{app_dir}}/app/cache 22 | mv {{app_dir}}/cache $dir 23 | rm -r $TMPDIR 24 | cd $dir 25 | 26 | rollback_msg() { 27 | arg=$? 28 | echo "> EXITED WITH ERROR" 29 | echo "> PLEASE ROLL BACK THE DEPLOY:" 30 | echo "> git push -f $oldrev:$refname" 31 | exit $arg 32 | } 33 | trap rollback_msg ERR 34 | 35 | # activate virtualenv 36 | . "{{app_dir}}/env/bin/activate" 37 | 38 | # should only do these when required 39 | pip install -qr "{{app_dir}}/app/requirements.txt" 40 | 41 | sudo systemctl restart fk-playout 42 | 43 | exit 0 44 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React from "react"; 3 | import { ComponentPropsWithoutRef } from "react"; 4 | 5 | const Container = styled.button<{ stretch?: boolean }>` 6 | display: inline-block; 7 | 8 | ${(props) => 9 | props.stretch && 10 | ` 11 | width: 100%; 12 | `} 13 | `; 14 | 15 | const Inner = styled.span` 16 | display: flex; 17 | width: 100%; 18 | 19 | user-select: none; 20 | `; 21 | 22 | export type ButtonProps = ComponentPropsWithoutRef<"button"> & { 23 | stretch?: boolean; 24 | }; 25 | 26 | function _Button(props: ButtonProps, ref: React.Ref) { 27 | const { stretch, className, children, type = "button", ...rest } = props; 28 | 29 | return ( 30 | 31 | {children} 32 | 33 | ); 34 | } 35 | 36 | export const Button = React.forwardRef(_Button); 37 | 38 | export type ButtonWithProps = ( 39 | props: ButtonProps & { ref?: React.Ref } & T 40 | ) => JSX.Element; 41 | -------------------------------------------------------------------------------- /packages/frontend/modules/styling/themes.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "@emotion/react"; 2 | 3 | export const lightTheme: Theme = { 4 | color: { 5 | background: "#fff", 6 | card: "rgba(255, 255, 255, 1)", 7 | accent: "#E88840", 8 | secondAccent: "#F15645", 9 | thirdAccent: "#64BC50", 10 | divider: "rgba(0, 0, 0, 0.2)", 11 | overlay: "rgba(0, 0, 0, 0.2)", 12 | }, 13 | stateColor: { 14 | success: "#46C400", 15 | warning: "#FFB657", 16 | danger: "#FF5A49", 17 | tip: "#FFE766", 18 | }, 19 | fontColor: { 20 | overlay: "rgba(255, 255, 255, 1)", 21 | normal: "rgba(0, 0, 0, 0.85)", 22 | muted: "rgba(0, 0, 0, 0.7)", 23 | subdued: "rgba(0, 0, 0, 0.5)", 24 | }, 25 | }; 26 | 27 | export const darkTheme: Theme = { 28 | ...lightTheme, 29 | color: { 30 | ...lightTheme.color, 31 | card: "#1c1c1c", 32 | background: "#131313", 33 | divider: "rgba(255, 255, 255, 0.2)", 34 | }, 35 | fontColor: { 36 | overlay: "rgba(255, 255, 255, 1)", 37 | normal: "rgba(255, 255, 255, 0.85)", 38 | muted: "rgba(255, 255, 255, 0.7)", 39 | subdued: "rgba(255, 255, 255, 0.5)", 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /infra/old/charts/frontend/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Release.Name }}-frontend 5 | namespace: {{ .Values.namespace }} 6 | spec: 7 | replicas: {{ .Values.replicas }} 8 | selector: 9 | matchLabels: 10 | app: frontend 11 | minReadySeconds: {{ .Values.minReadySeconds }} 12 | template: 13 | metadata: 14 | annotations: 15 | linkerd.io/inject: enabled 16 | labels: 17 | app: frontend 18 | spec: 19 | containers: 20 | - name: frontend 21 | image: {{ .Values.image.repository }}:{{ .Values.image.tag }} 22 | env: 23 | {{ toYaml .Values.env | indent 10 }} 24 | imagePullPolicy: {{ .Values.image.pullPolicy }} 25 | ports: 26 | - name: web 27 | containerPort: 3000 28 | readinessProbe: 29 | httpGet: 30 | path: /healthz 31 | port: 3000 32 | initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} 33 | livenessProbe: 34 | httpGet: 35 | path: /healthz 36 | port: 3000 37 | {{ toYaml .Values.probe | indent 10 }} 38 | -------------------------------------------------------------------------------- /infra/old/k8s/ingest/upload-processor-legacy/001-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: upload-processor-copy-to-legacy 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: upload-processor-copy-to-legacy 9 | template: 10 | metadata: 11 | labels: 12 | app: upload-processor-copy-to-legacy 13 | 14 | #annotations: 15 | # co.elastic.logs/json.add_error_key: "true" 16 | # co.elastic.logs/json.overwrite_keys: "true" 17 | # co.elastic.logs/json.keys_under_root: "true" 18 | spec: 19 | containers: 20 | - name: upload-processor-copy-to-legacy 21 | image: frikanalen/upload-processor-copy-to-legacy:latest 22 | imagePullPolicy: Always 23 | volumeMounts: 24 | - name: config-volume 25 | mountPath: /app/config 26 | - name: secrets-volume 27 | mountPath: /app/secrets 28 | volumes: 29 | - name: config-volume 30 | configMap: 31 | name: upload-processors 32 | - name: secrets-volume 33 | secret: 34 | secretName: upload-processors 35 | defaultMode: 0400 36 | -------------------------------------------------------------------------------- /packages/frontend/modules/video/components/LiveVideoPlayer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { AspectContainer } from "modules/core/components/AspectContainer"; 3 | import dynamic from "next/dynamic"; 4 | 5 | // No types for this library 6 | // @ts-ignore 7 | const ShakaPlayer = dynamic(() => import("shaka-player-react"), { ssr: false }) as any; 8 | 9 | const Container = styled.div` 10 | position: relative; 11 | 12 | > div { 13 | position: absolute; 14 | top: 0px; 15 | left: 0px; 16 | right: 0px; 17 | bottom: 0px; 18 | } 19 | 20 | border-radius: 4px; 21 | overflow: hidden; 22 | 23 | height: 100%; 24 | width: 100%; 25 | 26 | box-shadow: 2px 2px 11px 2px rgba(0, 0, 0, 0.1); 27 | `; 28 | 29 | export type LiveVideoPlayerProp = { 30 | src: string; 31 | width: number; 32 | height: number; 33 | }; 34 | 35 | export function LiveVideoPlayer(props: LiveVideoPlayerProp) { 36 | const { src, width, height } = props; 37 | 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /infra/old/k8s/frontend/frontend.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: frontend 5 | spec: 6 | replicas: 2 7 | selector: 8 | matchLabels: 9 | app: frontend 10 | minReadySeconds: 5 11 | template: 12 | metadata: 13 | annotations: 14 | linkerd.io/inject: enabled 15 | labels: 16 | app: frontend 17 | spec: 18 | containers: 19 | - name: frontend 20 | image: frikanalen/frontend:latest 21 | imagePullPolicy: Always 22 | ports: 23 | - name: web 24 | containerPort: 3000 25 | envFrom: 26 | - configMapRef: 27 | name: frontend-envs 28 | readinessProbe: 29 | httpGet: 30 | path: /healthz 31 | port: 3000 32 | initialDelaySeconds: 10 33 | livenessProbe: 34 | httpGet: 35 | path: /healthz 36 | port: 3000 37 | --- 38 | kind: Service 39 | apiVersion: v1 40 | metadata: 41 | name: frontend 42 | spec: 43 | type: ClusterIP 44 | selector: 45 | app: frontend 46 | ports: 47 | - protocol: TCP 48 | port: 3000 49 | name: web 50 | -------------------------------------------------------------------------------- /infra/old/k8s/streaming/002-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | #-movflags frag_keyframe+empty_moov 3 | #-map [s0] -crf 25 -b:v 500k -maxrate:v:0 700k -speed 7 4 | # -deadline realtime -codec:v libx264 -preset fast 5 | # -f mp4 udp://127.0.0.1:9001 6 | 7 | #-movflags frag_keyframe+empty_moov 8 | #-map [s1] -crf 25 -b:v 1500k -maxrate:v:0 1500k -speed 7 9 | # -deadline realtime -codec:v libx264 -preset fast 10 | # -f mp4 udp://127.0.0.1:9002 11 | kind: ConfigMap 12 | metadata: 13 | name: ffmpeg-config 14 | namespace: streaming 15 | data: 16 | ffmpeg_options: -probesize 100M -threads 10 -strict -2 17 | source_url: http://192.168.3.1:9094/frikanalen.ts 18 | encoding_options: >- 19 | -pix_fmt yuv420p -filter_complex [0:v]split=2[s1][s2] 20 | 21 | -map 0:1 -codec:a aac -f mpegts udp://127.0.0.1:9000 22 | 23 | -map [s1] -crf 25 -b:v 1000k -maxrate:v:0 1500k 24 | -profile:v baseline 25 | -deadline realtime -codec:v libx264 -preset fast 26 | -f mpegts udp://127.0.0.1:9002 27 | 28 | -map [s2] -crf 25 -b:v 2500k -maxrate:v:0 2500k -speed 7 29 | -deadline realtime -codec:v libx264 -preset fast 30 | -f mpegts udp://127.0.0.1:9003 31 | -------------------------------------------------------------------------------- /packages/frontend/modules/core/components/HeaderAuthBar.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { observer } from "mobx-react-lite"; 3 | import { spawnLoginModal } from "modules/auth/helpers/spawnLoginModal"; 4 | import { useManager } from "modules/state/manager"; 5 | import { GenericButton } from "modules/ui/components/GenericButton"; 6 | import React from "react"; 7 | import { HeaderUserDropdown } from "./HeaderUserDropdown"; 8 | 9 | const Container = styled.div` 10 | display: flex; 11 | justify-content: flex-end; 12 | flex: 1; 13 | `; 14 | 15 | export const HeaderAuthBar = observer(() => { 16 | const manager = useManager(); 17 | 18 | const { authStore } = manager.stores; 19 | const { isAuthenticated } = authStore; 20 | 21 | const renderUnauthenticated = () => { 22 | return spawnLoginModal(manager)} label="Logg inn" />; 23 | }; 24 | 25 | const renderAuthenticated = () => { 26 | const user = authStore.user!; 27 | 28 | return ; 29 | }; 30 | 31 | return {isAuthenticated ? renderAuthenticated() : renderUnauthenticated()}; 32 | }); 33 | -------------------------------------------------------------------------------- /packages/utils/stills-generator/chargen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from PIL import Image 3 | from PIL import ImageFont 4 | from PIL import ImageDraw 5 | import io 6 | 7 | class Poster(): 8 | def __init__(self, fields): 9 | self.heading_font = ImageFont.truetype('./Roboto-Black.ttf', 72) 10 | self.body_font = ImageFont.truetype('./Roboto-Black.ttf', 52) 11 | self.heading = fields['heading'] 12 | self.text = fields['text'] 13 | self.poster = Image.open('background.png') 14 | self.width, self.height = (1280, 720) 15 | 16 | def render(self): 17 | draw = ImageDraw.Draw(self.poster) 18 | 19 | draw.multiline_text((142, 180), self.heading, font=self.heading_font, fill='#d66969') 20 | draw.multiline_text((195, 300), self.text, font=self.body_font, fill='#78bddb') 21 | 22 | def _export(self, format): 23 | self.render() 24 | buf = io.BytesIO() 25 | self.poster.save(buf, format) 26 | return buf.getvalue() 27 | 28 | def getPNG(self): 29 | return self._export('PNG') 30 | 31 | def getRGBA(self): 32 | self.render() 33 | return self.poster.tobytes() 34 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/ScrollTrigger.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from "react"; 2 | import { useWindowEvent } from "../hooks/useWindowEvent"; 3 | 4 | export type ScrollTriggerProps = { 5 | direction?: "up" | "down"; 6 | onTrigger: (recheck: () => void) => void; 7 | }; 8 | 9 | const SCROLL_THRESHOLD = 150; 10 | 11 | export function ScrollTrigger(props: ScrollTriggerProps) { 12 | const { direction = "down", onTrigger } = props; 13 | const rootRef = useRef(null); 14 | 15 | const check = useCallback(() => { 16 | const { current: root } = rootRef; 17 | if (!root) return; 18 | 19 | const { top, bottom } = root.getBoundingClientRect(); 20 | const padding = SCROLL_THRESHOLD; 21 | 22 | if (direction === "up" && bottom < padding) { 23 | onTrigger(check); 24 | } 25 | 26 | if (direction === "down" && top - window.innerHeight < padding) { 27 | onTrigger(check); 28 | } 29 | }, [direction, onTrigger]); 30 | 31 | useWindowEvent("scroll", () => { 32 | check(); 33 | }); 34 | 35 | useEffect(() => { 36 | check(); 37 | }, [check]); 38 | 39 | return
; 40 | } 41 | -------------------------------------------------------------------------------- /packages/frontend/modules/ui/components/Section.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { PropsWithChildren } from "react"; 3 | import { IconType } from "../types"; 4 | import { SVGIcon } from "./SVGIcon"; 5 | 6 | const Container = styled.section``; 7 | 8 | const Header = styled.header` 9 | display: flex; 10 | align-items: center; 11 | 12 | margin-bottom: 16px; 13 | `; 14 | 15 | const Icon = styled(SVGIcon)` 16 | width: 20px; 17 | height: 20px; 18 | 19 | margin-right: 8px; 20 | `; 21 | 22 | const Title = styled.h1` 23 | font-size: 1em; 24 | font-weight: 500; 25 | `; 26 | 27 | export type SectionProps = PropsWithChildren<{ 28 | className?: string; 29 | title: string; 30 | icon?: IconType; 31 | }>; 32 | 33 | export function Section(props: SectionProps) { 34 | const { className, title, icon, children } = props; 35 | 36 | const renderIcon = () => { 37 | if (!icon) return null; 38 | 39 | return ; 40 | }; 41 | 42 | return ( 43 | 44 |
45 | {renderIcon()} 46 | {title} 47 |
48 | {children} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/frontend/modules/state/manager.ts: -------------------------------------------------------------------------------- 1 | import { IS_SERVER } from "modules/core/constants"; 2 | import React, { useContext } from "react"; 3 | import { StoreManager } from "./classes/StoreManager"; 4 | import { stores } from "./stores"; 5 | import { Manager } from "./types"; 6 | 7 | let manager: Manager; 8 | 9 | const createManager = () => new StoreManager(stores); 10 | 11 | export const getManager = (hydrationData?: object) => { 12 | if (IS_SERVER) { 13 | const ssrManager = createManager(); 14 | ssrManager.hydrate(hydrationData); 15 | 16 | return ssrManager; 17 | } 18 | 19 | if (!manager) { 20 | const clientManager = createManager(); 21 | clientManager.hydrate(hydrationData); 22 | 23 | manager = clientManager; 24 | } 25 | 26 | return manager; 27 | }; 28 | 29 | export const ManagerContext = React.createContext(undefined); 30 | 31 | export const useManager = () => { 32 | const manager = useContext(ManagerContext); 33 | 34 | if (!manager) { 35 | throw new Error("Manager not passed to context provider!"); 36 | } 37 | 38 | return manager; 39 | }; 40 | 41 | export const useStores = () => { 42 | return useManager().stores; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/schedule-service/playout.py: -------------------------------------------------------------------------------- 1 | from itertools import tee 2 | import json 3 | from database import Schedule 4 | from datetime import datetime 5 | import pytz 6 | import copy 7 | 8 | from database.schedule import Graphics 9 | def add_graphics(video_list): 10 | def pairwise(iterable): 11 | "s -> (s0,s1), (s1,s2), (s2, s3), ..." 12 | a, b = tee(iterable) 13 | next(b, None) 14 | return zip(a, b) 15 | 16 | graphics = [] 17 | 18 | for v, w in pairwise(video_list['items']): 19 | g = Graphics() 20 | g.end_time = copy.deepcopy(w.start_time) 21 | g.start_time = copy.deepcopy(v.end_time) 22 | duration = int((g.end_time - g.start_time).total_seconds() * 1000) 23 | g.URL = f'https://frikanalen.no/graphics/?duration={duration}' 24 | graphics.append(g) 25 | 26 | video_list['items'] = sorted(video_list['items'] + graphics, key=lambda x: x.start_time) 27 | return video_list 28 | 29 | def playout_schedule(): 30 | s = Schedule() 31 | video_list = add_graphics(s.get_date(datetime.now(tz=pytz.timezone('Europe/Oslo')))) 32 | return video_list 33 | 34 | if __name__=='__main__': 35 | print(playout_schedule()) 36 | -------------------------------------------------------------------------------- /infra/old/k8s/rook-ceph/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: Middleware 3 | metadata: 4 | name: scheme-redirect 5 | spec: 6 | redirectScheme: 7 | scheme: https 8 | permanent: true 9 | --- 10 | kind: IngressRoute 11 | apiVersion: traefik.containo.us/v1alpha1 12 | metadata: 13 | name: scheme-redirect-rook-ceph-dash 14 | namespace: rook-ceph 15 | spec: 16 | entryPoints: 17 | - web 18 | routes: 19 | - match: Host(`ceph.frikanalen.no`) 20 | priority: 100 21 | kind: Rule 22 | services: 23 | - kind: Service 24 | name: rook-ceph-mgr-dashboard 25 | namespace: rook-ceph 26 | scheme: http 27 | port: 7000 28 | middlewares: 29 | - name: scheme-redirect 30 | --- 31 | kind: IngressRoute 32 | apiVersion: traefik.containo.us/v1alpha1 33 | metadata: 34 | name: rook-ceph-dash 35 | namespace: rook-ceph 36 | spec: 37 | entryPoints: 38 | - websecure 39 | routes: 40 | - match: Host(`ceph.frikanalen.no`) 41 | kind: Rule 42 | services: 43 | - kind: Service 44 | name: rook-ceph-mgr-dashboard 45 | namespace: rook-ceph 46 | scheme: http 47 | port: 7000 48 | tls: 49 | certResolver: default 50 | -------------------------------------------------------------------------------- /packages/frontend/modules/core/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { ExternalLink } from "modules/ui/components/ExternalLink"; 3 | import { mainContentStyle } from "../styles/mainContentStyle"; 4 | 5 | export const Container = styled.footer` 6 | margin-top: 32px; 7 | margin-bottom: 32px; 8 | 9 | display: flex; 10 | justify-content: center; 11 | `; 12 | 13 | const Content = styled.div` 14 | ${mainContentStyle} 15 | 16 | display: flex; 17 | `; 18 | 19 | const Copyright = styled.span` 20 | flex: 1; 21 | color: ${(props) => props.theme.fontColor.muted}; 22 | `; 23 | 24 | const Links = styled.nav``; 25 | 26 | const Link = styled(ExternalLink)` 27 | margin-left: 16px; 28 | `; 29 | 30 | export function Footer() { 31 | return ( 32 | 33 | 34 | © 2009 - {new Date().getFullYear()} Foreningen Frikanalen 35 | 36 | GitHub 37 | API 38 | XMLTV 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /infra/old/k8s/streaming/003-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: Middleware 3 | metadata: 4 | name: cors 5 | namespace: streaming 6 | spec: 7 | headers: 8 | customResponseHeaders: 9 | Access-Control-Allow-Origin: "*" 10 | Access-Control-Allow-Headers: "range" 11 | Access-Control-Allow-Methods: "GET, POST, OPTIONS" 12 | --- 13 | apiVersion: traefik.containo.us/v1alpha1 14 | kind: Middleware 15 | metadata: 16 | name: strip-media-prefix 17 | namespace: streaming 18 | spec: 19 | stripPrefix: 20 | prefixes: 21 | - /stream 22 | --- 23 | kind: IngressRoute 24 | apiVersion: traefik.containo.us/v1alpha1 25 | metadata: 26 | name: fk-stream-https 27 | namespace: streaming 28 | spec: 29 | entryPoints: 30 | - websecure 31 | routes: 32 | - match: Host(`frikanalen.no`) && PathPrefix(`/stream/`) 33 | priority: 1000 34 | kind: Rule 35 | middlewares: 36 | - name: cors 37 | namespace: streaming 38 | - name: strip-media-prefix 39 | namespace: streaming 40 | services: 41 | - kind: Service 42 | name: shaka-packager 43 | namespace: streaming 44 | scheme: http 45 | port: 80 46 | tls: 47 | certResolver: default 48 | -------------------------------------------------------------------------------- /packages/frontend/modules/form/components/Form.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, PropsWithChildren } from "react"; 2 | import { ObservableForm } from "../classes/ObservableForm"; 3 | import React from "react"; 4 | import { FieldsProvider } from "./FieldsProvider"; 5 | 6 | export const formContext = createContext | undefined>(undefined); 7 | const { Provider } = formContext; 8 | 9 | export type FormProps = PropsWithChildren<{ 10 | className?: string; 11 | form: ObservableForm; 12 | onSubmit?: () => void; 13 | }>; 14 | 15 | export function Form(props: FormProps) { 16 | const { className, form, onSubmit, children } = props; 17 | 18 | const handleSubmit = (e: React.FormEvent) => { 19 | e.preventDefault(); 20 | 21 | if (onSubmit) onSubmit(); 22 | }; 23 | 24 | return ( 25 |
26 | 27 | {children} 28 | 29 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /infra/old/k8s/multiviewer-streaming/003-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: traefik.containo.us/v1alpha1 2 | kind: Middleware 3 | metadata: 4 | name: cors 5 | namespace: default 6 | spec: 7 | headers: 8 | customResponseHeaders: 9 | Access-Control-Allow-Origin: "*" 10 | Access-Control-Allow-Headers: "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With" 11 | Access-Control-Allow-Methods: "POST" 12 | --- 13 | apiVersion: traefik.containo.us/v1alpha1 14 | kind: Middleware 15 | metadata: 16 | name: strip-media-prefix 17 | namespace: streaming 18 | spec: 19 | stripPrefix: 20 | prefixes: 21 | - /stream 22 | --- 23 | kind: IngressRoute 24 | apiVersion: traefik.containo.us/v1alpha1 25 | metadata: 26 | name: fk-stream-https 27 | namespace: streaming 28 | spec: 29 | entryPoints: 30 | - websecure 31 | routes: 32 | - match: Host(`frikanalen.no`) && PathPrefix(`/stream/`) 33 | kind: Rule 34 | middlewares: 35 | - name: cors 36 | namespace: default 37 | - name: strip-media-prefix 38 | namespace: streaming 39 | services: 40 | - kind: Service 41 | name: shaka-packager 42 | scheme: http 43 | port: 80 44 | tls: 45 | certResolver: default 46 | -------------------------------------------------------------------------------- /packages/frontend/modules/video/resources/Video.ts: -------------------------------------------------------------------------------- 1 | import { Resource, ResourceFactory } from "modules/state/classes/Resource"; 2 | import { Manager } from "modules/state/types"; 3 | import { VideoData } from "../types"; 4 | 5 | export class Video extends Resource { 6 | constructor(data: VideoData, manager: Manager) { 7 | super(data, manager); 8 | 9 | const { organization } = this.data; 10 | const { organizationStore } = this.manager.stores; 11 | 12 | organizationStore.add(organization); 13 | } 14 | 15 | public get organization() { 16 | const { organizationStore } = this.manager.stores; 17 | return organizationStore.getResourceById(this.data.organization.id); 18 | } 19 | 20 | // TODO: This should perhaps be moved to the Organization resource in the future 21 | public get latestVideosByOrganization() { 22 | const { id } = this.organization.data; 23 | const { listStore } = this.manager.stores; 24 | 25 | return listStore.ensure(`latest-videos-organization-${id}`, "video", { 26 | path: "/videos", 27 | params: { 28 | organization: id, 29 | }, 30 | }); 31 | } 32 | } 33 | 34 | export const createVideo: ResourceFactory