Merge pull request #4 from mit-regressions/igel-t3-initial

Merge initial create-t3-app prototype
This commit is contained in:
Lucas Igel 2023-01-02 16:26:25 -06:00 committed by GitHub
commit 0136cdffa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 18332 additions and 525 deletions

4
.gitignore vendored
View File

@ -134,4 +134,6 @@ GitHub.sublime-settings
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history
.history
.DS_STORE
.vercel

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 MIT: REGRESSIONS
Copyright (c) 2022 REGRESSIONS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

12
Pipfile
View File

@ -1,12 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
django = "*"
[dev-packages]
[requires]
python_version = "3.8"

75
Pipfile.lock generated
View File

@ -1,75 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "99c4b9ec1b8891ff787677276760beb6d6d4919c55660da1c713682156a6086c"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.8"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"asgiref": {
"hashes": [
"sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4",
"sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"
],
"markers": "python_version >= '3.7'",
"version": "==3.5.2"
},
"backports.zoneinfo": {
"hashes": [
"sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf",
"sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328",
"sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546",
"sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6",
"sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570",
"sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9",
"sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7",
"sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987",
"sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722",
"sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582",
"sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc",
"sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b",
"sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1",
"sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08",
"sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac",
"sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"
],
"markers": "python_version < '3.9'",
"version": "==0.2.1"
},
"django": {
"hashes": [
"sha256:678bbfc8604eb246ed54e2063f0765f13b321a50526bdc8cb1f943eda7fa31f1",
"sha256:6b1de6886cae14c7c44d188f580f8ba8da05750f544c80ae5ad43375ab293cd5"
],
"index": "pypi",
"version": "==4.1.3"
},
"sqlparse": {
"hashes": [
"sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34",
"sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"
],
"markers": "python_version >= '3.5'",
"version": "==0.4.3"
},
"tzdata": {
"hashes": [
"sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d",
"sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"
],
"markers": "sys_platform == 'win32'",
"version": "==2022.7"
}
},
"develop": {}
}

View File

@ -1,2 +1,13 @@
# viewer
interactive player for MIT: REGRESSIONS, with footnotes, sources, and more
Currently using NextJS with create-t3-app and deploying to Vercel (https://vercel.com/regressions)
Uses react-player to play video in-browser, currently experimenting with [webvtt-player](https://github.com/umd-mith/webvtt-player) to display captions + sources + commentary + footnotes as the video plays!
TODO:
- create custom VTT schema with [OHMS support]() for our project's needs (implies ample unit testing and some E2E)
- create VTT builder so we can easily add sources (likely will be a light GUI, don't want to just do plain text editing of VTTs)
- create pretty caption playback
View current deployment at https://viewer-dukeeagle-regressions.vercel.app/

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "viewer.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

20
viewer/.env.example Normal file
View File

@ -0,0 +1,20 @@
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
# Keep this file up-to-date when you add new variables to `.env`.
# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
# When adding additional env variables, the schema in /env/schema.mjs should be updated accordingly
# Prisma
DATABASE_URL=file:./db.sqlite
# Next Auth
# You can generate the secret via 'openssl rand -base64 32' on Linux
# More info: https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
# Next Auth Discord Provider
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=

11
viewer/.eslintrc.json Normal file
View File

@ -0,0 +1,11 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint"],
"extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
"rules": {
"@typescript-eslint/consistent-type-imports": "warn"
}
}

42
viewer/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo

28
viewer/README.md Normal file
View File

@ -0,0 +1,28 @@
# Create T3 App
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
## What's next? How do I make an app with this?
We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
- [Next.js](https://nextjs.org)
- [NextAuth.js](https://next-auth.js.org)
- [Prisma](https://prisma.io)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
## Learn More
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
- [Documentation](https://create.t3.gg/)
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
## How do I deploy this?
Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class PlayerConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "player"

View File

@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,6 +0,0 @@
from . import views
from django.urls import path
urlpatterns = [
path("", views.player),
]

View File

@ -1,5 +0,0 @@
from django.shortcuts import render
# Create your views here.
def player(request):
return render(request, "player/player.html")

View File

@ -1,5 +0,0 @@
from django.contrib import admin
from . import models
# Register your models here.
admin.site.register(models.Annotation)
admin.site.register(models.Comment)

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "users"

View File

@ -1,20 +0,0 @@
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Annotation(models.Model):
id = models.AutoField(primary_key=True)
timestamp = models.CharField(max_length=20)
contents = models.CharField(max_length=1000)
def __str__(self):
return f"{self.timestamp}"
class Comment(models.Model):
id = models.AutoField(primary_key=True)
timestamp = models.CharField(max_length=20)
contents = models.CharField(max_length=1000)
author = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return f"{self.timestamp} @ {self.author}"

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,7 +0,0 @@
from . import views
from django.urls import path, include
urlpatterns = [
path("login/", views.login, name="login"),
path("signup/", views.signup, name="signup")
]

View File

@ -1,8 +0,0 @@
from django.shortcuts import render
# Create your views here.
def signup(request):
return render(request, "users/signup.html")
def login(request):
return render(request, "users/login.html")

View File

@ -1,16 +0,0 @@
"""
ASGI config for viewer project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "viewer.settings")
application = get_asgi_application()

25
viewer/next.config.mjs Normal file
View File

@ -0,0 +1,25 @@
// @ts-check
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
* This is especially useful for Docker builds.
*/
!process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs"));
/** @type {import("next").NextConfig} */
const config = {
reactStrictMode: true,
// TEMPORARY. will be removed once ref and other TS errors are fixed
typescript: {
// ignore typescript errors
ignoreBuildErrors: true,
},
swcMinify: true,
i18n: {
locales: ["en"],
defaultLocale: "en",
},
};
export default config;

7466
viewer/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
viewer/package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "viewer",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"postinstall": "prisma generate",
"lint": "next lint",
"start": "next start"
},
"dependencies": {
"@next-auth/prisma-adapter": "^1.0.5",
"@prisma/client": "^4.5.0",
"@tanstack/react-query": "^4.16.0",
"@trpc/client": "^10.0.0",
"@trpc/next": "^10.0.0",
"@trpc/react-query": "^10.0.0",
"@trpc/server": "^10.0.0",
"next": "13.1.1",
"next-auth": "^4.18.3",
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-player": "^2.11.0",
"superjson": "1.9.1",
"webvtt-player": "^0.0.16",
"zod": "^3.18.0"
},
"devDependencies": {
"@types/node": "^18.0.0",
"@types/prettier": "^2.7.2",
"@types/react": "^18.0.14",
"@types/react-dom": "^18.0.5",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"autoprefixer": "^10.4.7",
"eslint": "^8.26.0",
"eslint-config-next": "13.1.1",
"postcss": "^8.4.14",
"prettier": "^2.8.1",
"prettier-plugin-tailwindcss": "^0.2.1",
"prisma": "^4.5.0",
"tailwindcss": "^3.2.0",
"typescript": "^4.8.4"
},
"ct3aMetadata": {
"initVersion": "6.11.6"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1,4 @@
/** @type {import("prettier").Config} */
module.exports = {
plugins: [require.resolve("prettier-plugin-tailwindcss")],
};

View File

@ -0,0 +1,66 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
// NOTE: When using postgresql, mysql or sqlserver, uncomment the @db.Text annotations in model Account below
// Further reading:
// https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
// https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
url = env("DATABASE_URL")
}
model Example {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Necessary for Next auth
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? // @db.Text
access_token String? // @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? // @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}

Binary file not shown.

View File

@ -0,0 +1,40 @@
WEBVTT
Kind: captions
Language: en
00:00:14.068 --> 00:00:16.837
The Massachusetts Institute of Technology began with a promise.
00:00:19.252 --> 00:00:21.331
A promise that with a devotion to technology,
00:00:22.008 --> 00:00:23.735
through rigorous study of math and science,
00:00:23.939 --> 00:00:25.504
we can engineer a better tomorrow.
00:00:26.603 --> 00:00:27.599
we can make
00:00:27.599 --> 00:00:30.000
a Better World.
00:00:34.095 --> 00:00:35.347
In this story,
00:00:35.347 --> 00:00:37.242
we ask a simple question.
00:00:39.045 --> 00:00:40.283
Have we kept our promise?
00:01:07.526 --> 00:01:10.121
MIT was founded in 1861.
00:01:10.121 --> 00:01:13.448
But the Institute as we know it today was born in 1941.
00:01:13.448 --> 00:01:15.448
December 7th, 1941.

View File

@ -0,0 +1,412 @@
WEBVTT
Kind: captions
Language: en
NOTE separation between MUSIC SOURCE, VIDEO SOURCE, NARRATION SOURCE, COMMENTARY, and TRANSCRIPT.
00:00:01.550 --> 00:01:15.448
{
"uid": "1",
"type": "music",
"data": {
"type": "music",
"title": "The Vanishing American Family",
"artist": "ScubaZ",
"year": "2001",
"label": "Odd Records Ltd.",
"hyperlink": "https://www.discogs.com/master/1184747-Scuba-Z-The-Vanishing-American-Family"
}
}
00:00:01.560 --> 00:00:04.948
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "original footage",
"artist": "MIT: REGRESSIONS",
"year": "2022",
"notes": "",
"retrieved_from": "",
"hyperlink": ""
}
}
00:00:04.949 --> 00:00:12.948
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "MIT Holiday Greeting 2021",
"artist": "MIT Video Productions",
"year": "2021",
"notes": "",
"retrieved_from": "YouTube",
"hyperlink": "https://www.youtube.com/watch?v=7X2r6863KGA"
}
}
00:00:13.560 --> 00:00:15.559
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "original footage",
"artist": "MIT: REGRESSIONS",
"year": "2021",
"notes": "",
"retrieved_from": "",
"hyperlink": ""
}
}
00:00:15.560 --> 00:00:17.549
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "Fall around MIT",
"artist": "Andrei Ivanov",
"year": "2012",
"notes": "AI-upscaled and color-corrected",
"retrieved_from": "YouTube",
"hyperlink": "https://www.youtube.com/watch?v=xZXTpEJJxgs"
}
}
00:00:17.550 --> 00:00:19.548
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "\"We lived here, you didn't...\" @ Bexley Hall, MIT",
"artist": "Andrei Ivanov",
"year": "2013",
"notes": "AI-upscaled and frame-interpolated",
"retrieved_from": "YouTube",
"hyperlink": "https://www.youtube.com/watch?v=e6m8se96yyM&ab_channel=AndreiIvanov"
}
}
00:00:19.549 --> 00:00:24.200
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "Course 3 at MIT (confirm)",
"artist": "MIT Video Productions",
"year": "2019",
"notes": "",
"retrieved_from": "YouTube",
"hyperlink": "https://www.youtube.com/watch?v=e6m8se96yyM&ab_channel=AndreiIvanov"
}
}
00:00:24.201 --> 00:00:29.200
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "MIT.nano: An Overview",
"artist": "MIT",
"year": "2015",
"notes": "",
"retrieved_from": "YouTube",
"hyperlink": "https://www.youtube.com/watch?v=rGnr2ipfY3o"
}
}
00:00:29.201 --> 00:00:30.999
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "ZigZag Episode X",
"artist": "MIT",
"year": "200X",
"notes": "AI-upscaled",
"retrieved_from": "YouTube",
"hyperlink": ""
}
}
00:00:31.000 --> 00:00:32.900
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "ZigZag Episode X2",
"artist": "MIT",
"year": "200X",
"notes": "AI-upscaled and frame-interpolated",
"retrieved_from": "YouTube",
"hyperlink": ""
}
}
00:00:33.000 --> 00:00:36.000
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "A Celebration of Charles Vest",
"artist": "MIT Video Productions",
"year": "2005",
"notes": "AI-upscaled and color corrected",
"retrieved_from": "YouTube",
"hyperlink": ""
}
}
00:00:36.000 --> 00:00:38.100
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "MIT Project Athena: A Computer-Aided Teaching System",
"artist": "MIT Video Productions",
"year": "1984",
"notes": "AI-upscaled and color corrected",
"retrieved_from": "YouTube",
"hyperlink": "https://www.youtube.com/watch?v=tG7i7HCD9g0&t=34s"
}
}
00:00:38.101 --> 00:00:41.800
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "Afgan: The Soviet Experience",
"artist": "",
"year": "1989",
"notes": "AI-upscaled and color corrected",
"retrieved_from": "YouTube",
"hyperlink": "https://www.youtube.com/watch?v=tG7i7HCD9g0&t=34s"
}
}
00:00:41.801 --> 00:00:42.800
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "Chemistry Lab at MIT",
"artist": "WGBH",
"year": "1980s",
"notes": "AI-upscaled",
"retrieved_from": "WGBH Open Vault",
"hyperlink": ""
}
}
00:00:43.001 --> 00:00:44.000
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "Jugglers at MIT",
"artist": "(confirm)",
"year": "1970s",
"notes": "AI-upscaled and color-corrected",
"retrieved_from": "YouTube",
"hyperlink": ""
}
}
00:00:44.001 --> 00:00:45.600
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "The November Actions",
"artist": "Ricky Leacock",
"year": "1969",
"notes": "AI-upscaled and frame-interpolated",
"retrieved_from": "MIT Museum",
"hyperlink": ""
}
}
00:00:45.601 --> 00:00:47.499
{
"uid": "3",
"type": "video_source",
"data": {
"type": "video_source",
"title": "MIT: Progressions",
"artist": "David and Sheri Espar",
"year": "1969",
"notes": "AI-upscaled and frame-interpolated",
"retrieved_from": "Kenneth Friedman (YouTube)",
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
}
}
00:00:47.500 --> 00:00:48.600
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "The November Actions",
"artist": "Ricky Leacock",
"year": "1969",
"notes": "AI-upscaled and frame-interpolated",
"retrieved_from": "MIT Museum",
"hyperlink": ""
}
}
00:00:48.601 --> 00:00:50.900
{
"uid": "2",
"type": "video_source",
"data": {
"type": "video_source",
"title": "MIT Centennial Procession",
"artist": "MIT",
"year": "1961",
"notes": "AI-upscaled and color-corrected",
"retrieved_from": "MIT Museum",
"hyperlink": ""
}
}
00:00:50.901 --> 00:00:52.200
{
"uid": "3",
"type": "video_source",
"data": {
"type": "video_source",
"title": "MIT: Progressions",
"artist": "David and Sheri Espar",
"year": "1969",
"notes": "AI-upscaled and frame-interpolated",
"retrieved_from": "Kenneth Friedman (YouTube)",
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
}
}
00:00:52.201 --> 00:00:53.999
{
"uid": "3",
"type": "video_source",
"data": {
"type": "video_source",
"title": "MIT AI Lab Films",
"artist": "MIT",
"year": "1950s",
"notes": "Color-corrected",
"retrieved_from": "MIT",
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
}
}
00:00:54.000 --> 00:00:55.000
{
"uid": "3",
"type": "video_source",
"data": {
"type": "video_source",
"title": "MIT: Progressions",
"artist": "David and Sheri Espar",
"year": "1969",
"notes": "AI-upscaled and frame-interpolated",
"retrieved_from": "Kenneth Friedman (YouTube)",
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
}
}
00:00:55.100 --> 00:00:55.800
{
"uid": "3",
"type": "video_source",
"data": {
"type": "video_source",
"title": "The Social Beaver",
"artist": "Oscar H. Horovitz",
"year": "1956",
"notes": "AI-upscaled and color-corrected",
"retrieved_from": "MIT Museum",
"hyperlink": "https://www.youtube.com/watch?v=X4TriKZrszw"
}
}
00:00:55.950 --> 00:01:01.600
{
"uid": "3",
"type": "video_source",
"data": {
"type": "video_source",
"title": "MIT: Progressions",
"artist": "David and Sheri Espar",
"year": "1969",
"notes": "AI-upscaled and frame-interpolated",
"retrieved_from": "Kenneth Friedman (YouTube)",
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
}
}
00:01:01.700 --> 00:01:02.899
{
"uid": "3",
"type": "video_source",
"data": {
"type": "video_source",
"title": "The Social Beaver",
"artist": "Oscar H. Horovitz",
"year": "1956",
"notes": "AI-upscaled and color-corrected",
"retrieved_from": "MIT Museum",
"hyperlink": "https://www.youtube.com/watch?v=X4TriKZrszw"
}
}
00:01:02.900 --> 00:01:03.799
{
"uid": "3",
"type": "video_source",
"data": {
"type": "video_source",
"title": "Making Electrons Count",
"artist": "Lloyd C. Sanford",
"year": "c. 1950",
"notes": "AI-upscaled and color-corrected",
"retrieved_from": "MIT Museum",
"hyperlink": "https://www.youtube.com/watch?v=KH0tcv3nEQI"
}
}
00:01:03.800 --> 00:01:15.600
{
"uid": "3",
"type": "video_source",
"data": {
"type": "video_source",
"title": "Technology",
"artist": "MIT",
"year": "1934",
"notes": "AI-upscaled and frame-interpolated. View Kathleen E. 23's <a href=\"https://mitadmissions.org/blogs/entry/5-historical-mit-videos/#video5\"><strong>post</strong></a>",
"retrieved_from": "MIT Museum",
"hyperlink": "https://www.youtube.com/watch?v=rsO_67xzymQ&ab_channel=FromtheVaultofMIT"
}
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

BIN
viewer/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1 +0,0 @@
secret.py

View File

@ -1,130 +0,0 @@
"""
Django settings for viewer project.
Generated by 'django-admin startproject' using Django 4.1.3.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "viewer.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [BASE_DIR / 'templates'],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "viewer.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_ROOT = BASE_DIR / 'serve/'
STATIC_URL = '/static/'
STATICFILES_DIRS = (
BASE_DIR / 'static/',
)
MEDIA_ROOT = BASE_DIR / 'media/'
MEDIA_URL = '/media/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
try:
from .secret import *
except ImportError:
pass

View File

@ -1,2 +0,0 @@
SECRET_KEY=secret
DEBUG=True

View File

@ -0,0 +1,38 @@
import ReactPlayer from "react-player/lazy";
// import CustomReactPlayer from "./CustomReactPlayer";
interface PlayerRef {
seeking: boolean;
played: number;
duration: number;
seekTo: (time: number) => void;
}
// inherit prop playing from parent component
export default function VideoPlayer({ playerRef, playing, videoUrl, transcriptUrl }: { playerRef: React.RefObject<PlayerRef>, playing: boolean, videoUrl: string, transcriptUrl: string }) {
// ReactPlayer.addCustomPlayer(CustomReactPlayer);
return (
<ReactPlayer
ref={playerRef}
crossOrigin="anonymous"
playing={playing}
controls={true}
// url="https://youtu.be/TGKk3iwoI9I"
url={videoUrl}
onSeek={e => console.log('onSeek', e)}
config={{
file: {
forceVideo: true,
tracks: [
{
label: 'English',
kind: 'captions',
src: transcriptUrl,
srcLang: 'en',
default: true,
},
],
},
}}
/>
);
}

View File

@ -0,0 +1,34 @@
import React, { Component } from 'react'
import MetadataPoint from './MetadataPoint'
class Metadata extends Component<MetadataProps> {
render() {
const lines = []
if (this.props.track && this.props.track.cues) {
for (let i = 0; i < this.props.track.cues.length; i++) {
lines.push(
<MetadataPoint
key={`point-${i}`}
cue={this.props.track.cues[i]}
active={false}
seek={this.props.seek} />
)
}
}
return (
<div className="track">
{lines}
</div>
)
}
}
type MetadataProps = {
url: string,
track: TextTrack,
seek: (time: number) => void
}
export default Metadata

View File

@ -0,0 +1,212 @@
import React, { Component } from 'react'
// import './MetadataPoint.css'
type MetadataPointProps = {
cue: VTTCue
active: boolean
seek: (time: number) => void
}
interface MetadataPointData {
"uid": string,
"type": string,
// data is one of four interfaces: music, commentary, transcript, or video_source
"data": MusicData | CommentaryData | TranscriptData | VideoSourceData | NarrationSourceData | OhmsData
}
interface MusicData {
"type": "music",
"title": string,
"title_alt": string,
"artist": string,
"year": string,
"label": string,
"hyperlink": string,
}
interface CommentaryData {
"type": "commentary",
"text": string, // may contain VTT-compatible styling, as specified at https://www.w3.org/TR/webvtt1/#model-overview
}
interface TranscriptData {
"type": "transcript",
"text": string, // may contain VTT-compatible styling, as specified at https://www.w3.org/TR/webvtt1/#model-overview
}
interface VideoSourceData {
"type": "video_source",
"title": string,
"artist": string, // optional - may be empty string
"attribution": string,
"year": string,
"notes": string,
"retrieved_from": string,
"hyperlink": string,
}
interface NarrationSourceData {
"type": "narration_source",
"title": string,
"attribution": string,
"year": string,
"retrieval_date": string,
"source_type" : string, // PDF, etc
"hyperlink": string,
}
// adopting the OHMS standard http://ohda.matrix.msu.edu/2014/11/indexing-interviews-in-ohms/
interface OhmsData {
"type": "ohms",
"title": string,
"title_alt": string,
"synopsis": string,
"synopsis_alt": string,
"keywords": string,
"keywords_alt": string,
"subjects": string,
"subjects_alt": string,
"gpspoints": {
"gps": string,
"gps_zoom": string,
"gps_text": string,
"gps_text_alt": string
},
"hyperlinks": {
"hyperlink": string,
"hyperlink_text": string,
"hyperlink_text_alt": string
}
}
class MetadataPoint extends Component<MetadataPointProps, { isActive: boolean}> {
constructor(props: MetadataPointProps) {
super(props)
this.state = {
isActive: false
}
this.props.cue.onenter = this.onEnter.bind(this)
this.props.cue.onexit = this.onExit.bind(this)
this.onClick = this.onClick.bind(this)
// get current theme (but in a class component)
}
render() {
let style = ''
if (this.state.isActive) {
// active
style = "bg-gray-200"
}
// exect JSON.parse data to be of type MetadataPointData
const point = JSON.parse(this.props.cue.text) as MetadataPointData
const data = point.data
let song = null
let footage = null
// get type of data
if (point.type == "music") {
// const song is point.data as MusicData
song = data as MusicData
}
if (point.type == "video_source") {
footage = data as VideoSourceData
}
return (
<div className={`point ${style}`}>
<div className="time" onClick={this.onClick}>
[{this.startTime()} - {this.endTime()}]
</div>
<div className="text">
{point.type == "music" &&
<div className="music" onClick={this.onClick}>
<SongCard song={song} />
</div>
}
{point.type == "video_source" &&
<div className="footage" onClick={this.onClick}>
<VideoSourceCard videoSource={footage} />
</div>
}
</div>
</div>
)
}
onClick() {
this.props.seek(this.props.cue.startTime)
}
startTime() {
return this.formatSeconds(this.props.cue.startTime)
}
endTime() {
return this.formatSeconds(this.props.cue.endTime)
}
onEnter() {
this.setState({isActive: true})
}
onExit() {
this.setState({isActive: false})
}
formatSeconds(t) {
let mins = Math.floor(t / 60)
if (mins < 10) {
mins = `0${mins}`
}
let secs = Math.floor(t % 60)
if (secs < 10) {
secs = `0${secs}`
}
return `${mins}:${secs}`
}
}
const VideoSourceCard: React.FC<VideoSourceData> = ({videoSource}) => {
return (
<div className="bg-white shadow-md p-4">
<h2 className="text-lg font-bold">{videoSource.title}</h2>
<div className="text-gray-600">
<p className="mb-1">{videoSource.artist}</p>
<p className="mb-1">{videoSource.year}</p>
{/* render the html that may be inside of {videoSource.notes} */}
<div className="mb-1" dangerouslySetInnerHTML={{__html: videoSource.notes}} />
</div>
<a
href={videoSource.hyperlink}
className="top-0 right-0 bottom-0 bg-gray-500 hover:bg-gray-400 text-white text-xs font-bold px-3 py-1"
>
Link
</a>
</div>
);
};
// TODO: fix typing
const SongCard: React.FC<MusicData> = ({song}) => {
return (
<div className="bg-yellow-50 shadow-md p-4">
<h2 className="text-lg font-bold">{song.title}</h2>
<div className="text-gray-600">
<p className="mb-1">{song.artist}</p>
<p className="mb-1">{song.year}</p>
<p className="mb-1">{song.label}</p>
</div>
</div>
);
};
export default MetadataPoint

View File

@ -0,0 +1,25 @@
.search {
border-top: thin solid #ccc;
padding: 5px 5px 5px 5px;
font-size: 14pt;
margin-top: 5px;
}
.search .container {
border: thin solid #ccc;
}
.search input {
font-size: 12pt;
border: none;
width: 90%;
}
.search input:focus {
outline: none;
}
.search .icon {
padding-left: 5px;
padding-right: 5px;
}

View File

@ -0,0 +1,24 @@
import React, { FunctionComponent } from "react" // TODO: fix warning
import styles from "./Search.module.css"
const Search: FunctionComponent<SearchProps> = ({ query, updateQuery }) => {
return (
<div className={styles.search}>
<div className={styles.container}>
<span className={styles.icon}>🔍</span>
<input
value={query}
onChange={e => updateQuery(e.target.value)} />
</div>
</div>
)
}
// set prop types using TypeScript
type SearchProps = {
query: string,
updateQuery: (query: string) => void
}
export default Search

View File

@ -0,0 +1,37 @@
import React, { Component } from 'react'
import TranscriptLine from './TranscriptLine'
// import './Track.css' // currently exists in global instead. TODO: consolidate this styling
class Transcript extends Component<TranscriptProps> {
render() {
const lines = []
if (this.props.track && this.props.track.cues) {
for (let i = 0; i < this.props.track.cues.length; i++) {
lines.push(
<TranscriptLine
key={`line-${i}`}
cue={this.props.track.cues[i]}
active={false}
seek={this.props.seek}
query={this.props.query} />
)
}
}
return (
<div className="track">
{lines}
</div>
)
}
}
type TranscriptProps = {
track: TextTrack,
url: string,
seek: (time: number) => void,
query: string
}
export default Transcript

View File

@ -0,0 +1,79 @@
import React, { Component } from 'react'
import './TranscriptLine.module.css'
class TranscriptLine extends Component<TranscriptLineProps, { isActive: boolean }> {
constructor(props: TranscriptLineProps) {
super(props)
this.state = {
isActive: false
}
this.props.cue.onenter = this.onEnter.bind(this)
this.props.cue.onexit = this.onExit.bind(this)
this.onClick = this.onClick.bind(this)
}
render() {
let style = ''
if (this.props.query && this.props.cue.text.match(new RegExp(this.props.query, 'i'))) {
style = 'match'
} else if (this.state.isActive) {
style = 'active'
}
// note: dangerouslySetInnerHTML is used because the text may contain HTML
return (
<div className={`${style} line`} onClick={this.onClick}>
<div className="time">
[{this.startTime()} - {this.endTime()}]
</div>
<div
className={`${style} text`}
dangerouslySetInnerHTML={{__html: this.props.cue.text}} />
</div>
)
}
onEnter() {
this.setState({isActive: true})
}
onExit() {
this.setState({isActive: false})
}
onClick() {
this.props.seek(this.props.cue.startTime)
}
startTime() {
return this.formatSeconds(this.props.cue.startTime)
}
endTime() {
return this.formatSeconds(this.props.cue.endTime)
}
formatSeconds(t) {
let mins = Math.floor(t / 60)
if (mins < 10) {
mins = `0${mins}`
}
let secs = Math.floor(t % 60)
if (secs < 10) {
secs = `0${secs}`
}
return `${mins}:${secs}`
}
}
type TranscriptLineProps = {
cue: TextTrackCue,
seek: (time: number) => void,
query: string
}
export default TranscriptLine

View File

@ -0,0 +1,168 @@
import { useLayoutEffect, useState, useEffect, useRef } from 'react';
import dynamic from "next/dynamic";
// const ReactPlayer = dynamic(() => import("react-player/lazy"), { ssr: false });
import Transcript from './Transcript'
import Metadata from './Metadata'
import Search from './Search'
const VideoPlayer = dynamic(() => import("../VideoPlayer"), { ssr: false });
type WebVttPlayerProps = {
audio: string,
videoUrl: string, // TODO: make naming scheme consistent lol
transcript: string,
metadataUrl: string,
preload: boolean,
};
interface ReactPlayerRef {
seeking: boolean;
played: number;
duration: number;
seekTo: (time: number) => void;
}
interface NativePlayerRef {
currentTime: number;
ended: boolean;
loop: boolean;
muted: boolean;
play: () => void;
}
//Write a fetcher function to wrap the native fetch function and return the result of a call to url in json format
// const fetcher = (url: string) => fetch(url).then((res) => res.json());
export default function WebVttPlayer(props: WebVttPlayerProps) {
const [trackLoaded, setTrackLoaded] = useState(false);
const [metatrackLoaded, setMetatrackLoaded] = useState(false);
const [query, setQuery] = useState('');
// TODO: determine if these should be set
const [playing, setPlaying] = useState(false);
const trackRef = useRef<TrackEvent>(null);
const metatrackRef = useRef<TrackEvent>(null);
const reactPlayerRef = useRef<ReactPlayerRef>(null);
const nativePlayerRef = useRef<NativePlayerRef>(null);
const preload = props.preload ? "true" : "false"
//There are 3 possible states: (1) loading when data is null (2) ready when the data is returned (3) error when there was an error fetching the data
// const { data, error } = useSWR('/api/staticdata', fetcher);
//Handle the error state
// if (error) console.log("Failed to load from SWR!"); //return <div>Failed to load</div>;
//Handle the loading state
// if (!data) console.log("Loading with SWR...")// return <div>Loading...</div>;
useEffect(() => {
// Get a reference to the track and metatrack elements
// TODO: this manual timeout is extremely gross! figure out how to conditionally rendder <Transcript> and <Metadata> according to loading of these refs without this awful hard-coded thing. NextJs certainly supports something better for on-time ref loading (SWR? getProps?)
function checkIfLoaded(tries = 0) {
tries += 1
const track = trackRef.current;
const metatrack = metatrackRef.current;
if (track && track.track && track.track.cues && track.track.cues.length > 0) {
setTrackLoaded(true);
}
if (metatrack && metatrack.track && metatrack.track.cues && metatrack.track.cues.length > 0) {
setMetatrackLoaded(true);
}
else if (!metatrackLoaded || !trackLoaded) {
const wait = 25 * Math.pow(tries, 2)
setTimeout(() => checkIfLoaded(tries), wait);
}
}
checkIfLoaded();
}, []);
// if we figure out how to get access to Track refs with react-player (even in YouTube videos! That would be awesome), then we can start using this
function reactPlayerSeek(secs: string) {
if (reactPlayerRef.current) {
reactPlayerRef.current.seekTo(parseFloat(secs))
}
setPlaying(true);
}
function seek(secs: number) {
if (nativePlayerRef.current) {
nativePlayerRef.current.currentTime = secs;
nativePlayerRef.current.play();
}
setPlaying(true);
}
return (
<>
<div className="flex w-full overflow-hidden">
<div className="w-1/2 player">
{/* <VideoPlayer playerRef={video} playing={playing} videoUrl={props.videoUrl} transcriptUrl={props.transcript} /> */}
{/* a vanilla video element with source and tracks. so much easier oh my god */}
<video
// width="75%"
preload={preload}
// ignore ref errors for now
ref={nativePlayerRef}
crossOrigin="anonymous"
controls={true}
autoPlay={true}
>
<source src={props.videoUrl} type="video/mp4" />
<track
ref={trackRef}
kind="subtitles"
src={props.transcript}
srcLang="en"
default={true}
/>
<track default
kind="metadata"
src={props.metadataUrl}
ref={metatrackRef} />
</video>
</div>
<div className="w-1/2">
<div className="webvtt-player">
<div className="media h-1\/3">
<div className="tracks">
{trackLoaded ? (
<>
<Transcript
url={props.transcript}
seek={seek}
track={trackRef.current.track}
query={query}
/>
</>
) : (
"Loading transcript..."
)}
{metatrackLoaded && props.metadataUrl ? (
<>
<Metadata
url={props.metadataUrl}
seek={seek}
track={metatrackRef.current.track}
/>
</>
) : (
"\nLoading metadata..." // TODO: make better logic for showing if we wanna serve metadata
)}
</div>
</div>
</div>
</div>
</div>
</>
);
}

23
viewer/src/pages/_app.tsx Normal file
View File

@ -0,0 +1,23 @@
import { type AppType } from "next/app";
import { type Session } from "next-auth";
import { SessionProvider } from "next-auth/react";
import { trpc } from "../utils/trpc";
import {ThemeProvider} from 'next-themes'
import "../styles/globals.css";
const MyApp: AppType<{ session: Session | null }> = ({
Component,
pageProps: { session, ...pageProps },
}) => {
return (
<SessionProvider session={session}>
<ThemeProvider attribute="class">
<Component {...pageProps} />
</ThemeProvider>
</SessionProvider>
);
};
export default trpc.withTRPC(MyApp);

View File

@ -0,0 +1,30 @@
import NextAuth, { type NextAuthOptions } from "next-auth";
import DiscordProvider from "next-auth/providers/discord";
// Prisma adapter for NextAuth, optional and can be removed
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { env } from "../../../env/server.mjs";
import { prisma } from "../../../server/db/client";
export const authOptions: NextAuthOptions = {
// Include user.id on session
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
}
return session;
},
},
// Configure one or more authentication providers
adapter: PrismaAdapter(prisma),
providers: [
DiscordProvider({
clientId: env.DISCORD_CLIENT_ID,
clientSecret: env.DISCORD_CLIENT_SECRET,
}),
// ...add more providers here
],
};
export default NextAuth(authOptions);

View File

@ -0,0 +1,10 @@
import { type NextApiRequest, type NextApiResponse } from "next";
import { prisma } from "../../server/db/client";
const examples = async (req: NextApiRequest, res: NextApiResponse) => {
const examples = await prisma.example.findMany();
res.status(200).json(examples);
};
export default examples;

View File

@ -0,0 +1,21 @@
import { type NextApiRequest, type NextApiResponse } from "next";
import { getServerAuthSession } from "../../server/common/get-server-auth-session";
const restricted = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getServerAuthSession({ req, res });
if (session) {
res.send({
content:
"This is protected content. You can access this content because you are signed in.",
});
} else {
res.send({
error:
"You must be signed in to view the protected content on this page.",
});
}
};
export default restricted;

View File

@ -0,0 +1,17 @@
import { createNextApiHandler } from "@trpc/server/adapters/next";
import { env } from "../../../env/server.mjs";
import { createContext } from "../../../server/trpc/context";
import { appRouter } from "../../../server/trpc/router/_app";
// export API handler
export default createNextApiHandler({
router: appRouter,
createContext,
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(`❌ tRPC failed on ${path}: ${error}`);
}
: undefined,
});

View File

@ -0,0 +1,88 @@
import React, { useRef } from "react";
import { type NextPage } from "next";
import Head from "next/head";
import { useRouter } from 'next/router';
import { signIn, signOut, useSession } from "next-auth/react";
import { trpc } from "../utils/trpc";
import { useTheme } from 'next-themes'
import WebVttPlayer from "../components/WebVttPlayer/WebVttPlayer";
const Home: NextPage = () => {
const hello = trpc.example.hello.useQuery({ text: "from tRPC" });
const { theme, setTheme } = useTheme()
const router = useRouter();
const videoUrl = '/data/MIT Regressions intro video.mp4' // BIZARRE BUG: react-player component only works when I live-reload URL to valid path. if Chrome loads directly, then returns "failed to load media".
const audioUrl = router.asPath + 'data/MIT Regressions intro audio.mp3'
const transcriptUrl = router.asPath + "data/MIT Regressions intro captions.vtt"
const metadataUrl = router.asPath + "data/MIT Regressions intro metadata.vtt"
return (
<>
<Head>
<title>viewer</title>
<meta name="description" content="Generated by create-t3-app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-black">
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 py-2 px-4 absolute top-4 right-4">
toggle theme
</button>
<div className="px-4 py-32 pb-0 position-absolute">
<div className="titles">
<h1 className="text-1xl font-extrabold tracking-tight text-black dark:text-white sm:text-[3rem] top-4 left-4 absolute">
documentary metadata viewer
</h1>
{/* hug the left-hand side of the screen */}
<h2 className=" tracking-tight text-gray-500 dark:text-white top-20 left-6 absolute"><i>current film</i>: <a href="https://www.youtube.com/watch?v=TGKk3iwoI9I&ab_channel=MIT%3AREGRESSIONS">MIT: REGRESSIONS intro</a> &nbsp; | &nbsp; <a href="https://github.com/mit-regressions/viewer/blob/igel-t3-initial/viewer/public/data/MIT%20Regressions%20intro%20metadata.vtt">metadata source</a></h2>
{/* <h2 className=" tracking-tight text-gray-500 dark:text-white top-24 left-6 absolute"><i>metadata file source (VTT)</i>: <a href="https://www.youtube.com/watch?v=TGKk3iwoI9I&ab_channel=MIT%3AREGRESSIONS">MIT: REGRESSIONS intro</a></h2> */}
</div>
<WebVttPlayer
preload={true}
audio={audioUrl}
videoUrl={videoUrl}
transcript={transcriptUrl}
metadataUrl={metadataUrl}
/>
{/* <div className="flex flex-col items-center gap-2">
<AuthShowcase />
</div> */}
</div>
</main>
</>
);
}
export default Home;
const AuthShowcase: React.FC = () => {
const { data: sessionData } = useSession();
const { data: secretMessage } = trpc.auth.getSecretMessage.useQuery(
undefined, // no input
{ enabled: sessionData?.user !== undefined },
);
return (
<div className="flex flex-col items-center justify-center gap-4">
<p className="text-center text-2xl text-white">
{sessionData && <span>Logged in as {sessionData.user?.name}</span>}
{secretMessage && <span> - {secretMessage}</span>}
</p>
<button
className="bg-white/10 px-10 py-3 font-semibold text-white no-underline transition hover:bg-white/20"
onClick={sessionData ? () => signOut() : () => signIn()}
>
{sessionData ? "Sign out" : "Sign in"}
</button>
</div>
);
};

View File

@ -0,0 +1,15 @@
import { type GetServerSidePropsContext } from "next";
import { unstable_getServerSession } from "next-auth";
import { authOptions } from "../../pages/api/auth/[...nextauth]";
/**
* Wrapper for unstable_getServerSession https://next-auth.js.org/configuration/nextjs
* See example usage in trpc createContext or the restricted API route
*/
export const getServerAuthSession = async (ctx: {
req: GetServerSidePropsContext["req"];
res: GetServerSidePropsContext["res"];
}) => {
return await unstable_getServerSession(ctx.req, ctx.res, authOptions);
};

View File

@ -0,0 +1,19 @@
import { PrismaClient } from "@prisma/client";
import { env } from "../../env/server.mjs";
declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
export const prisma =
global.prisma ||
new PrismaClient({
log:
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
if (env.NODE_ENV !== "production") {
global.prisma = prisma;
}

View File

@ -0,0 +1,39 @@
import { type inferAsyncReturnType } from "@trpc/server";
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
import { type Session } from "next-auth";
import { getServerAuthSession } from "../common/get-server-auth-session";
import { prisma } from "../db/client";
type CreateContextOptions = {
session: Session | null;
};
/** Use this helper for:
* - testing, so we dont have to mock Next.js' req/res
* - trpc's `createSSGHelpers` where we don't have req/res
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
**/
export const createContextInner = async (opts: CreateContextOptions) => {
return {
session: opts.session,
prisma,
};
};
/**
* This is the actual context you'll use in your router
* @link https://trpc.io/docs/context
**/
export const createContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
// Get the session from the server using the unstable_getServerSession wrapper function
const session = await getServerAuthSession({ req, res });
return await createContextInner({
session,
});
};
export type Context = inferAsyncReturnType<typeof createContext>;

View File

@ -0,0 +1,11 @@
import { router } from "../trpc";
import { authRouter } from "./auth";
import { exampleRouter } from "./example";
export const appRouter = router({
example: exampleRouter,
auth: authRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;

View File

@ -0,0 +1,10 @@
import { router, publicProcedure, protectedProcedure } from "../trpc";
export const authRouter = router({
getSession: publicProcedure.query(({ ctx }) => {
return ctx.session;
}),
getSecretMessage: protectedProcedure.query(() => {
return "you can now see this secret message!";
}),
});

View File

@ -0,0 +1,16 @@
import { z } from "zod";
import { router, publicProcedure } from "../trpc";
export const exampleRouter = router({
hello: publicProcedure
.input(z.object({ text: z.string().nullish() }).nullish())
.query(({ input }) => {
return {
greeting: `Hello ${input?.text ?? "world"}`,
};
}),
getAll: publicProcedure.query(({ ctx }) => {
return ctx.prisma.example.findMany();
}),
});

View File

@ -0,0 +1,39 @@
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { type Context } from "./context";
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape }) {
return shape;
},
});
export const router = t.router;
/**
* Unprotected procedure
**/
export const publicProcedure = t.procedure;
/**
* Reusable middleware to ensure
* users are logged in
*/
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: { ...ctx.session, user: ctx.session.user },
},
});
});
/**
* Protected procedure
**/
export const protectedProcedure = t.procedure.use(isAuthed);

View File

@ -0,0 +1,165 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.webvtt-player {
/* hug the left-hand side of the screen */
/* @apply text-purple-500; */
}
.track {
/* flex: 6;
height: 90vh;
padding: 15px;
overflow-y: scroll; */
/* implement the above styling with tailwind */
/* tailwind flex-CUSTOMNUMBER */
/* @apply h-96 p-[15px] overflow-y-scroll flex-[6]; */
/* @apply text-orange-200; /* this works lol */
}
}
/* Player */
.webvtt-player .media {
width: 100%;
border: thin solid #ccc;
margin-right: auto;
}
.webvtt-player audio {
width: 100%;
background-color: #ccc;
}
.webvtt-player audio::-webkit-media-controls-panel {
background-color: #ccc;
}
.webvtt-player .tracks {
display: flex;
}
.webvtt-player .controls {
padding: 5px;
text-align: center;
vertical-align: text-top;
background-color: #ccc;
}
.webvtt-player .seekBar {
vertical-align: middle;
width: 70%;
}
/* Track */
.webvtt-player .track {
flex: 6;
height: 90vh;
padding: 10px;
overflow-y: scroll;
}
/* TranscriptLine */
.webvtt-player .active {
/* background-color: #eee; */
/* light border */
@apply bg-gray-200 border-gray-600 border-2 border-opacity-10 border-x-0;
}
.webvtt-player .match {
/* background-color: lightyellow; */
@apply bg-gray-200;
}
.webvtt-player .line {
padding: 5px;
cursor: pointer;
}
.webvtt-player .time {
width: 110px;
float: left;
font-size: 10pt;
line-height: 14pt;
}
.webvtt-player .text {
margin-left: 110px;
}
/* Metadatapoint */
.webvtt-player .point {
padding: 5px;
}
.webvtt-player .time {
width: 110px;
float: left;
font-size: 10pt;
line-height: 14pt;
cursor: pointer;
}
.webvtt-player .text {
margin-left: 110px;
}
.webvtt-player .title {
font-weight: bold;
font-size: inherit;
margin: 0;
cursor: pointer;
}
.webvtt-player .titleAlt {
font-style: italic;
font-weight: normal;
font-size: inherit;
margin: 0;
}
.webvtt-player .field {
margin-top: 5px;
}
.webvtt-player .field span {
font-style: italic;
}
/* Search */
.search {
border-top: thin solid #ccc;
padding: 5px 5px 5px 5px;
font-size: 14pt;
margin-top: 5px;
}
.search .container {
border: thin solid #ccc;
}
.search input {
font-size: 12pt;
border: none;
width: 90%;
}
.search input:focus {
outline: none;
}
.search .icon {
padding-left: 5px;
padding-right: 5px;
}

12
viewer/src/types/next-auth.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
import { type DefaultSession } from "next-auth";
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
user?: {
id: string;
} & DefaultSession["user"];
}
}

42
viewer/src/utils/trpc.ts Normal file
View File

@ -0,0 +1,42 @@
import { httpBatchLink, loggerLink } from "@trpc/client";
import { createTRPCNext } from "@trpc/next";
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
import superjson from "superjson";
import { type AppRouter } from "../server/trpc/router/_app";
const getBaseUrl = () => {
if (typeof window !== "undefined") return ""; // browser should use relative url
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
};
export const trpc = createTRPCNext<AppRouter>({
config() {
return {
transformer: superjson,
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
}),
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
};
},
ssr: false,
});
/**
* Inference helper for inputs
* @example type HelloInput = RouterInputs['example']['hello']
**/
export type RouterInputs = inferRouterInputs<AppRouter>;
/**
* Inference helper for outputs
* @example type HelloOutput = RouterOutputs['example']['hello']
**/
export type RouterOutputs = inferRouterOutputs<AppRouter>;

View File

@ -1,55 +0,0 @@
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
:root {
--background: #FCEFC2
}
body {
background: var(--background)
}
* {
box-sizing: border-box;
}
body {
font-family: Arial, Helvetica, sans-serif;
}
/* Float four columns side by side */
.column {
float: left;
width: 25%;
padding: 0 10px;
}
/* Remove extra left and right margins, due to padding */
.row {margin: 0 -5px;}
/* Clear floats after the columns */
.row:after {
content: "";
display: table;
clear: both;
}
/* Responsive columns */
@media screen and (max-width: 600px) {
.column {
width: 100%;
display: block;
margin-bottom: 20px;
}
}
/* Style the counter cards */
.card {
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
padding: 16px;
text-align: center;
background-color: #f1f1f1;
}

View File

@ -1,11 +0,0 @@
form {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
background: none;
width: 100%;
height: 100%;
}

View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
fontFamily: {
sans: ['HelveticaNeue', 'Helvetica'],
},
extend: {},
},
plugins: [],
};

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="author" content="Rushil Umaretiya" />
<title>MIT Regressions</title>
<link rel="stylesheet" href="{% static 'css/base.css' %}" />
{% block head %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</html>

View File

@ -1,38 +0,0 @@
{% extends "player/base.html" %}
{% block head %}
<link href="https://vjs.zencdn.net/7.19.2/video-js.css" rel="stylesheet" />
<style>
.vjs-blue-theme .vjs-play-progress {
background: #0000ff;
}
</style>
{% endblock %}
{% block body %}
<video
id="my-player"
class="video-js"
controls
preload="auto"
poster="https://vjs.zencdn.net/v/oceans.png"
data-setup='{}'>
<source src="https://vjs.zencdn.net/v/oceans.mp4" type="video/mp4"></source>
<source src="https://vjs.zencdn.net/v/oceans.webm" type="video/webm"></source>
<source src="https://vjs.zencdn.net/v/oceans.ogv" type="video/ogg"></source>
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a
web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">
supports HTML5 video
</a>
</p>
</video>
<div class="row">
<div class="column">
<div class="card">
<p>timestamp</p>
<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,14 +0,0 @@
{% extends "player/base.html" %}
{% load static %}
{% block head %}
<link rel="stylesheet" href="{% static 'css/registration.css' %}" />
{% endblock %}
{% block body %}
<form method="post">
<h1>login</h1>
{% csrf_token %}
{{ form.as_p }}
<button type="submit">login</button>
</form>
{% endblock %}

View File

@ -1,14 +0,0 @@
{% extends "player/base.html" %}
{% load static %}
{% block head %}
<link rel="stylesheet" href="{% static 'css/registration.css' %}" />
{% endblock %}
{% block body %}
<form method="post">
<h1>register</h1>
{% csrf_token %}
{{ form.as_p }}
<button type="submit">sign up</button>
</form>
{% endblock %}

21
viewer/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noUncheckedIndexedAccess": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs", "webvtt-plaer.d.ts"],
"exclude": ["node_modules"]
}

View File

@ -1,23 +0,0 @@
"""viewer URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts", include("viewer.apps.users.urls")),
path("", include("viewer.apps.player.urls")),
]

1
viewer/webvtt-player.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'webvtt-player';

View File

@ -1,16 +0,0 @@
"""
WSGI config for viewer project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "viewer.settings")
application = get_wsgi_application()