├── .dockerignore ├── .github └── workflows │ └── docker.yaml ├── .gitignore ├── .prettierrc.json ├── Dockerfile ├── README.md ├── doc ├── DEPLOY.md ├── create_indices.js └── indices │ ├── youtube-archive-page-views.json │ ├── youtube-archive-searches.json │ └── youtube-archive.json ├── helmchart ├── Chart.yaml ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── ingress.yaml │ ├── pdb.yaml │ └── service.yaml └── values.yaml ├── helmfile.yaml ├── modules ├── AboutPage.tsx ├── ApiDocsPage.tsx ├── ChannelPage.tsx ├── ChannelsListPage.tsx ├── ChatExplorerPage.tsx ├── CommentSection.tsx ├── CustomPlayerPage.tsx ├── ExpandableContainer.tsx ├── LandingPage.tsx ├── SearchPage.tsx ├── ServiceUnavailablePage.tsx ├── SpeedTestPage.tsx ├── StatusPage.tsx ├── VideoEmbedPage.tsx ├── WatchPage.tsx └── shared │ ├── ChatReplay │ ├── ChatMessageRender.tsx │ ├── ChatReplay.tsx │ ├── ChatReplayPanel.tsx │ └── parser │ │ ├── default.ts │ │ ├── index.ts │ │ └── yt-dlp.ts │ ├── ClientRender.tsx │ ├── DefaultHead.tsx │ ├── Header.tsx │ ├── MemoLinkify.tsx │ ├── NextImage.tsx │ ├── PageBase.tsx │ ├── PaginatedResults.tsx │ ├── Sidebar.tsx │ ├── VideoActionButtons.tsx │ ├── VideoCard.tsx │ ├── VideoPlayer │ ├── MediaSync.tsx │ ├── SeekBar.tsx │ ├── VideoPlayer.tsx │ ├── VideoPlayer2.tsx │ └── components │ │ └── LoaderRing.tsx │ ├── VideoPlayerHead.tsx │ ├── config.ts │ ├── database.d.ts │ ├── database.ts │ ├── fileAuth.ts │ ├── format.ts │ ├── hooks │ ├── useAnimationFrame.tsx │ ├── useDebounce.tsx │ ├── useLocalStorage.tsx │ ├── useThrottle.tsx │ └── useWindowSize.tsx │ ├── icons.tsx │ ├── util.ts │ └── validation.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── about.tsx ├── api-docs.tsx ├── api │ ├── pv.ts │ ├── sitemap │ │ ├── channel │ │ │ └── [channelId].ts │ │ └── index.xml.ts │ ├── v1 │ │ ├── channels.ts │ │ ├── search.ts │ │ └── status.ts │ ├── v2 │ │ └── archive │ │ │ └── [videoId].ts │ └── video.ts ├── channel │ └── [channelId].tsx ├── channels.tsx ├── embed │ └── [videoId].tsx ├── index.tsx ├── player.tsx ├── search.tsx ├── speedtest.tsx ├── status.tsx ├── tools │ └── chat-explorer.tsx └── watch.tsx ├── postcss.config.js ├── public ├── favicon.ico └── favicon.png ├── styles ├── player.css └── tailwind.css ├── tailwind.config.js ├── tsconfig.json ├── values.prod.yaml └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .env* 2 | .next 3 | .git 4 | .secrets.yaml 5 | .dockerignore 6 | node_modules/ 7 | README.md 8 | Dockerfile 9 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | 12 | jobs: 13 | docker: 14 | strategy: 15 | matrix: 16 | platform: 17 | - linux/amd64 18 | - linux/arm64 19 | 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Docker meta 26 | id: meta 27 | uses: docker/metadata-action@v5 28 | with: 29 | images: ghcr.io/${{ github.repository }} 30 | flavor: latest=false,suffix=-${{ matrix.platform }} 31 | 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v3 34 | 35 | - name: Login to GHCR 36 | uses: docker/login-action@v3 37 | with: 38 | registry: ghcr.io 39 | username: ${{ github.actor }} 40 | password: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Build and push 43 | uses: docker/build-push-action@v6 44 | with: 45 | context: . 46 | push: ${{ github.event_name != 'pull_request' }} 47 | tags: ${{ steps.meta.outputs.tags }} 48 | labels: ${{ steps.meta.outputs.labels }} 49 | platforms: ${{ matrix.platform }} 50 | cache-from: type=gha 51 | cache-to: type=gha,mode=max 52 | 53 | build-multi-architecture: 54 | needs: 55 | - docker 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: Login to GHCR 59 | uses: docker/login-action@v3 60 | with: 61 | registry: ghcr.io 62 | username: ${{ github.actor }} 63 | password: ${{ secrets.GITHUB_TOKEN }} 64 | 65 | - name: Docker meta 66 | id: metadata 67 | uses: docker/metadata-action@v5 68 | with: 69 | images: ghcr.io/${{ github.repository }} 70 | 71 | - uses: int128/docker-manifest-create-action@v1 72 | with: 73 | tags: ${{ steps.metadata.outputs.tags }} 74 | suffixes: | 75 | -linux-amd64 76 | -linux-arm64 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | .envrc 33 | .secrets.yaml 34 | 35 | # vercel 36 | .vercel 37 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "proseWrap": "always" 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install dependencies only when needed 2 | FROM node:alpine AS deps 3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 4 | RUN apk add --no-cache libc6-compat 5 | WORKDIR /app 6 | COPY package.json yarn.lock ./ 7 | RUN yarn install --frozen-lockfile 8 | 9 | # Rebuild the source code only when needed 10 | FROM node:alpine AS builder 11 | WORKDIR /app 12 | COPY . . 13 | COPY --from=deps /app/node_modules ./node_modules 14 | RUN yarn build && yarn install --production --ignore-scripts --prefer-offline 15 | 16 | # Production image, copy all the files and run next 17 | FROM node:alpine AS runner 18 | WORKDIR /app 19 | 20 | ENV NODE_ENV production 21 | 22 | RUN addgroup -g 1001 -S nodejs && \ 23 | adduser -S nextjs -u 1001 24 | 25 | COPY --from=builder /app/doc ./doc 26 | COPY --from=builder /app/next.config.js ./ 27 | COPY --from=builder /app/public ./public 28 | COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next 29 | COPY --from=builder /app/node_modules ./node_modules 30 | COPY --from=builder /app/package.json ./package.json 31 | 32 | USER nextjs 33 | 34 | EXPOSE 3000 35 | 36 | CMD ["yarn", "start"] 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # archive-browser 2 | 3 | Frontend for [Ragtag Archive](https://archive.ragtag.moe). 4 | 5 | ## Maintenance status 6 | 7 | Ragtag Archive (the backend and this repository) is currently feature-frozen 8 | until there is sufficient manpower to undo the accumulated technical debt. 9 | 10 | ## Deploy 11 | 12 | Check out [aonahara/archive-docker](https://gitlab.com/aonahara/archive-docker) 13 | to quickly set up a local copy of Ragtag Archive. 14 | 15 | If you want to use this frontend for your own purposes, you can check the config 16 | file at 17 | [`modules/shared/config.ts`](https://github.com/ragtag-archive/archive-browser/blob/master/modules/shared/config.ts), 18 | and make appropriate changes to connect it to your backend. For more info, read 19 | [`doc/DEPLOY.md`](https://github.com/ragtag-archive/archive-browser/blob/master/doc/DEPLOY.md). 20 | 21 | Note that this project only includes the web interface, and does not include 22 | other parts such as archival tools. You should use your own tools to archive and 23 | index content. 24 | -------------------------------------------------------------------------------- /doc/DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Deploy Instructions 2 | 3 | The following are instructions to deploy the frontend manually. If you want to 4 | get things up and running quickly, a dockerized setup is available at 5 | [aonahara/archive-docker](https://gitlab.com/aonahara/archive-docker). 6 | 7 | For a minimum deployment, this frontend requres the following to function: 8 | 9 | - An Elasticsearch 7.11 database 10 | - A separate URL to serve files 11 | 12 | ## Preparation 13 | 14 | ### Create the database 15 | 16 | Set up an Elasticsearch 7.11 database. You can quickly deploy it using Docker: 17 | 18 | ```bash 19 | $ docker run -p 9200:9200 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.11.2 20 | ``` 21 | 22 | Note, the above command creates a **temporary** database for testing. All data 23 | **will be gone** after a reboot. For more information, including how to deploy 24 | the database persistently, refer to the 25 | [official documentation](https://www.elastic.co/guide/en/elasticsearch/reference/7.11/docker.html). 26 | 27 | Once the database is up, you should be able to query it like so: 28 | 29 | ```bash 30 | $ curl http://localhost:9200 31 | ``` 32 | 33 | Which should yield an output similar to this: 34 | 35 | ```json 36 | { 37 | "name": "my-instance", 38 | "cluster_name": "my-cluster", 39 | "cluster_uuid": "...", 40 | "version": { 41 | "number": "7.11.2", 42 | "build_flavor": "default", 43 | "build_type": "docker", 44 | "build_hash": "...", 45 | "build_date": "...", 46 | "build_snapshot": false, 47 | "lucene_version": "8.7.0", 48 | "minimum_wire_compatibility_version": "6.8.0", 49 | "minimum_index_compatibility_version": "6.0.0-beta1" 50 | }, 51 | "tagline": "You Know, for Search" 52 | } 53 | ``` 54 | 55 | If you see that output, the database is ready. 56 | 57 | ### Create indices and mappings 58 | 59 | You should see JSON files in the `doc/indices` folder of this repository. Those 60 | files contain the mappings and settings necessary to create the indices. To 61 | create the indices, run the `create_indices` script: 62 | 63 | ```bash 64 | node ./create_indices.js http://localhost:9200 65 | ``` 66 | 67 | The database is now ready. 68 | 69 | ### Prepare the file server 70 | 71 | The frontend expects a file server which serves data with the following 72 | structure: 73 | 74 | ``` 75 | / 76 | / 77 | .chat.json 78 | .mkv 79 | .f248.webm 80 | .f251.webm 81 | .webp 82 | / 83 | profile.jpg 84 | ``` 85 | 86 | For instance, if your files are served from 87 | `https://archive-content.ragtag.moe/`, the frontend will attempt to fetch 88 | channel profile images from 89 | `https://archive-content.ragtag.moe/UCRMpIxnySp7Fy5SbZ8dBv2w/profile.jpg`. Video 90 | thumbnails will be fetched from 91 | `https://archive-content.ragtag.moe/tE-J5q8OCF0/tE-J5q8OCF0.webp`, and so on. 92 | 93 | Videos on the `/watch?v=` page are streamed, so make sure your file server 94 | accepts the `Range` header to allow for seeking. You should also make sure to 95 | include appropriate CORS headers if you decide to host the files under a 96 | different domain name. 97 | 98 | You can use a webserver such as `nginx` for this purpose. But for testing, let's 99 | use a temporary server using the `http-server` package from npm. Run the 100 | following command in the folder which contains the files as described above. 101 | 102 | ``` 103 | $ npx http-server -p 8080 --cors 104 | ``` 105 | 106 | The above command will create a basic HTTP file server, which will serve files 107 | from the current folder. 108 | 109 | ### Updating the configuration 110 | 111 | Open up the file `./modules/shared/config.ts`. You should see various options, 112 | its descriptions, and sometimes its default value. To override the options, you 113 | can set environment variables using a file called `.env.local`. Create the file 114 | `.env.local` in this project directory and fill it in with the following values: 115 | 116 | ``` 117 | ES_INDEX=youtube-archive 118 | ES_BACKEND_URL=http://localhost:9200 119 | ES_AUTHORIZATION= 120 | DRIVE_BASE_URL=http://localhost:8080 121 | ENABLE_SIGN_URLS=false 122 | ``` 123 | 124 | ### Insert some data 125 | 126 | For the website to be useful, you should have some data in your Elasticsearch 127 | database. Create the file `data.json` with the following content: 128 | 129 | ```json 130 | { 131 | "video_id": "Y1So82y91Yw", 132 | "channel_name": "フブキCh。白上フブキ", 133 | "channel_id": "UCdn5BQ06XqgXoAxIhbqw5Rg", 134 | "upload_date": "2019-10-24", 135 | "title": "Im. Scatman", 136 | "description": "im scatman", 137 | "duration": 57, 138 | "width": 886, 139 | "height": 1920, 140 | "fps": 30, 141 | "format_id": "313+140", 142 | "view_count": 3466630, 143 | "like_count": 202661, 144 | "dislike_count": 2723, 145 | "files": [ 146 | { 147 | "name": "Y1So82y91Yw.info.json", 148 | "size": 286517 149 | }, 150 | { 151 | "name": "Y1So82y91Yw.mkv", 152 | "size": 26116973 153 | }, 154 | { 155 | "name": "Y1So82y91Yw.jpg", 156 | "size": 78964 157 | }, 158 | { 159 | "name": "Y1So82y91Yw.f313.webm", 160 | "size": 25182161 161 | }, 162 | { 163 | "name": "Y1So82y91Yw.f140.m4a", 164 | "size": 916875 165 | }, 166 | { 167 | "name": "Y1So82y91Yw.webp", 168 | "size": 34392 169 | } 170 | ], 171 | "archived_timestamp": "2021-03-15T02:31:17.878Z" 172 | } 173 | ``` 174 | 175 | Then perform the insert: 176 | 177 | ```bash 178 | $ curl -XPUT -H 'Content-Type: application/json' -d '@data.json' http://localhost:9200/youtube-archive/_doc/Y1So82y91Yw 179 | ``` 180 | 181 | Be sure to also grab all the files and put them in the folder where you did the 182 | `npx http-server` thing. You can get the files from 183 | [here](https://archive.ragtag.moe/watch?v=Y1So82y91Yw). Click the three dots 184 | besides the Download button to get the various files. 185 | 186 | ## Run it! 187 | 188 | Once everything is prepared, run `yarn` to install all the dependencies. When 189 | done, run `yarn dev` to start a dev server. If successful, you should be able to 190 | open `http://localhost:3000` in your browser and see the video you just 191 | inserted. 192 | -------------------------------------------------------------------------------- /doc/create_indices.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const [cmd, file, ...args] = process.argv; 5 | 6 | if (args.length < 1) { 7 | console.error('Usage: create_indices.js '); 8 | process.exit(1); 9 | } 10 | 11 | const baseURL = args[0]; 12 | 13 | async function run() { 14 | const files = fs.readdirSync(path.join(__dirname, './indices')); 15 | 16 | for (const file of files) { 17 | const fileContents = fs.readFileSync( 18 | path.join(__dirname, './indices', file) 19 | ); 20 | const indexName = file.split('.')[0]; 21 | console.log('Creating index', indexName); 22 | const response = await axios 23 | .request({ 24 | baseURL, 25 | url: '/' + indexName, 26 | method: 'PUT', 27 | data: JSON.parse(fileContents), 28 | }) 29 | .catch((err) => err.response); 30 | console.log(response.data); 31 | } 32 | } 33 | 34 | run(); 35 | -------------------------------------------------------------------------------- /doc/indices/youtube-archive-page-views.json: -------------------------------------------------------------------------------- 1 | { 2 | "mappings": { 3 | "properties": { 4 | "timestamp": { 5 | "type": "date" 6 | }, 7 | "channel_id": { 8 | "type": "keyword" 9 | }, 10 | "video_id": { 11 | "type": "keyword" 12 | }, 13 | "ip": { 14 | "type": "ip" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /doc/indices/youtube-archive-searches.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "analysis": { 4 | "analyzer": { 5 | "autocomplete": { 6 | "tokenizer": "autocomplete", 7 | "filter": ["lowercase"] 8 | }, 9 | "autocomplete_search": { 10 | "tokenizer": "lowercase" 11 | } 12 | }, 13 | "tokenizer": { 14 | "autocomplete": { 15 | "type": "edge_ngram", 16 | "min_gram": 2, 17 | "max_gram": 10, 18 | "token_chars": ["letter"] 19 | } 20 | } 21 | } 22 | }, 23 | "mappings": { 24 | "properties": { 25 | "query": { 26 | "type": "text", 27 | "analyzer": "autocomplete", 28 | "search_analyzer": "autocomplete_search", 29 | "fields": { 30 | "keyword": { 31 | "type": "keyword" 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /doc/indices/youtube-archive.json: -------------------------------------------------------------------------------- 1 | { 2 | "mappings": { 3 | "properties": { 4 | "archived_timestamp": { 5 | "type": "date" 6 | }, 7 | "channel_id": { 8 | "type": "keyword" 9 | }, 10 | "channel_name": { 11 | "type": "text" 12 | }, 13 | "description": { 14 | "type": "text" 15 | }, 16 | "dislike_count": { 17 | "type": "long" 18 | }, 19 | "duration": { 20 | "type": "integer" 21 | }, 22 | "files": { 23 | "type": "nested", 24 | "properties": { 25 | "name": { 26 | "type": "keyword" 27 | }, 28 | "size": { 29 | "type": "long" 30 | } 31 | } 32 | }, 33 | "format_id": { 34 | "type": "keyword" 35 | }, 36 | "fps": { 37 | "type": "integer" 38 | }, 39 | "height": { 40 | "type": "integer" 41 | }, 42 | "like_count": { 43 | "type": "long" 44 | }, 45 | "title": { 46 | "type": "text" 47 | }, 48 | "upload_date": { 49 | "type": "date", 50 | "format": "yyyy-MM-dd" 51 | }, 52 | "timestamps": { 53 | "properties": { 54 | "publishedAt": { 55 | "type": "date" 56 | }, 57 | "scheduledStartTime": { 58 | "type": "date" 59 | }, 60 | "actualStartTime": { 61 | "type": "date" 62 | }, 63 | "actualEndTime": { 64 | "type": "date" 65 | } 66 | } 67 | }, 68 | "video_id": { 69 | "type": "keyword" 70 | }, 71 | "view_count": { 72 | "type": "long" 73 | }, 74 | "width": { 75 | "type": "integer" 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /helmchart/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: archive-browser 2 | version: v0.0.1 3 | description: 'Web UI for Ragtag Archive' 4 | home: https://github.com/ragtag-archive/archive-browser 5 | dependencies: 6 | - name: common 7 | version: 1.7.1 8 | repository: https://charts.bitnami.com/bitnami/ 9 | -------------------------------------------------------------------------------- /helmchart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "archivebrowser.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "archivebrowser.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "archivebrowser.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "archivebrowser.labels" -}} 38 | helm.sh/chart: {{ include "archivebrowser.chart" . }} 39 | {{ include "archivebrowser.selectorLabels" . }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end -}} 45 | 46 | {{/* 47 | Selector labels 48 | */}} 49 | {{- define "archivebrowser.selectorLabels" -}} 50 | app.kubernetes.io/name: {{ include "archivebrowser.name" . }} 51 | app.kubernetes.io/instance: {{ .Release.Name }} 52 | {{- end -}} 53 | 54 | {{/* 55 | Create the name of the service account to use 56 | */}} 57 | {{- define "archivebrowser.serviceAccountName" -}} 58 | {{- if .Values.serviceAccount.create -}} 59 | {{ default (include "archivebrowser.fullname" .) .Values.serviceAccount.name }} 60 | {{- else -}} 61 | {{ default "default" .Values.serviceAccount.name }} 62 | {{- end -}} 63 | {{- end -}} 64 | 65 | -------------------------------------------------------------------------------- /helmchart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "archivebrowser.fullname" . }} 5 | labels: 6 | {{- include "archivebrowser.labels" . | nindent 4 }} 7 | {{- if .Values.commonLabels }} 8 | {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} 9 | {{- end }} 10 | {{- if .Values.commonAnnotations }} 11 | annotations: 12 | {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | replicas: {{ .Values.replicaCount }} 16 | selector: 17 | matchLabels: 18 | {{- include "archivebrowser.selectorLabels" . | nindent 6 }} 19 | template: 20 | metadata: 21 | labels: 22 | {{- include "archivebrowser.selectorLabels" . | nindent 8 }} 23 | {{- if .Values.podLabels }} 24 | {{- include "common.tplvalues.render" ( dict "value" .Values.podLabels "context" $ ) | nindent 8 }} 25 | {{- end }} 26 | {{- if .Values.commonLabels }} 27 | {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 8 }} 28 | {{- end }} 29 | {{- if or .Values.podAnnotations .Values.commonAnnotations }} 30 | annotations: 31 | {{- if .Values.podAnnotations }} 32 | {{- include "common.tplvalues.render" ( dict "value" .Values.podAnnotations "context" $ ) | nindent 8 }} 33 | {{- end }} 34 | {{- if .Values.commonAnnotations }} 35 | {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 8 }} 36 | {{- end }} 37 | {{- end }} 38 | spec: 39 | {{- with .Values.imagePullSecrets }} 40 | imagePullSecrets: 41 | {{- toYaml . | nindent 8 }} 42 | {{- end }} 43 | containers: 44 | - name: archivebrowser 45 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 46 | imagePullPolicy: {{ .Values.image.pullPolicy }} 47 | {{- if .Values.extraEnvVars }} 48 | env: 49 | {{- include "common.tplvalues.render" (dict "value" .Values.extraEnvVars "context" $) | nindent 12 }} 50 | {{- end }} 51 | {{- if or .Values.extraEnvVarsCM .Values.extraEnvVarsSecret }} 52 | envFrom: 53 | {{- if .Values.extraEnvVarsCM }} 54 | - configMapRef: 55 | name: {{ include "common.tplvalues.render" (dict "value" .Values.extraEnvVarsCM "context" $) }} 56 | {{- end }} 57 | {{- if .Values.extraEnvVarsSecret }} 58 | - secretRef: 59 | name: {{ include "common.tplvalues.render" (dict "value" .Values.extraEnvVarsSecret "context" $) }} 60 | {{- end }} 61 | {{- end }} 62 | ports: 63 | - name: http 64 | containerPort: 3000 65 | protocol: TCP 66 | {{- if .Values.livenessProbe.enabled }} 67 | livenessProbe: 68 | httpGet: 69 | path: /status 70 | port: http 71 | initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} 72 | periodSeconds: {{ .Values.livenessProbe.periodSeconds }} 73 | timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} 74 | failureThreshold: {{ .Values.livenessProbe.failureThreshold }} 75 | successThreshold: {{ .Values.livenessProbe.successThreshold }} 76 | {{- end }} 77 | {{- if .Values.readinessProbe.enabled }} 78 | readinessProbe: 79 | httpGet: 80 | path: /status 81 | port: http 82 | initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} 83 | periodSeconds: {{ .Values.readinessProbe.periodSeconds }} 84 | timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} 85 | failureThreshold: {{ .Values.readinessProbe.failureThreshold }} 86 | successThreshold: {{ .Values.readinessProbe.successThreshold }} 87 | {{- end }} 88 | resources: 89 | {{- toYaml .Values.resources | nindent 12 }} 90 | {{- with .Values.nodeSelector }} 91 | nodeSelector: 92 | {{- toYaml . | nindent 8 }} 93 | {{- end }} 94 | {{- with .Values.affinity }} 95 | affinity: 96 | {{- toYaml . | nindent 8 }} 97 | {{- end }} 98 | {{- with .Values.tolerations }} 99 | tolerations: 100 | {{- toYaml . | nindent 8 }} 101 | {{- end }} 102 | 103 | -------------------------------------------------------------------------------- /helmchart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | apiVersion: {{ include "common.capabilities.ingress.apiVersion" . }} 3 | kind: Ingress 4 | metadata: 5 | name: {{ include "archivebrowser.fullname" . }} 6 | labels: 7 | {{- include "archivebrowser.labels" . | nindent 4 }} 8 | {{- if .Values.commonLabels }} 9 | {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} 10 | {{- end }} 11 | {{- if or .Values.ingress.annotations .Values.commonAnnotations }} 12 | annotations: 13 | {{- if .Values.ingress.annotations }} 14 | {{- include "common.tplvalues.render" ( dict "value" .Values.ingress.annotations "context" $ ) | nindent 4 }} 15 | {{- end }} 16 | {{- if .Values.commonAnnotations }} 17 | {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} 18 | {{- end }} 19 | {{- end }} 20 | spec: 21 | {{- if and .Values.ingress.ingressClassName (eq "true" (include "common.ingress.supportsIngressClassname" .)) }} 22 | ingressClassName: {{ .Values.ingress.ingressClassName | quote }} 23 | {{- end }} 24 | {{- if .Values.ingress.tls }} 25 | tls: 26 | {{- range .Values.ingress.tls }} 27 | - hosts: 28 | {{- range .hosts }} 29 | - {{ . | quote }} 30 | {{- end }} 31 | secretName: {{ .secretName }} 32 | {{- end }} 33 | {{- end }} 34 | rules: 35 | {{- range .Values.ingress.hosts }} 36 | - host: {{ .host | quote }} 37 | http: 38 | paths: 39 | {{- range .paths }} 40 | - path: {{ . }} 41 | {{- if eq "true" (include "common.ingress.supportsPathType" $) }} 42 | pathType: {{ $.Values.ingress.pathType }} 43 | {{- end }} 44 | backend: 45 | {{- include "common.ingress.backend" (dict "serviceName" (include "archivebrowser.fullname" $) "servicePort" $.Values.service.port "context" $) | nindent 14 }} 46 | {{- end }} 47 | {{- end }} 48 | {{- end }} 49 | 50 | -------------------------------------------------------------------------------- /helmchart/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.pdb.create }} 2 | apiVersion: {{ include "common.capabilities.policy.apiVersion" . }} 3 | kind: PodDisruptionBudget 4 | metadata: 5 | name: {{ include "archivebrowser.fullname" . }} 6 | labels: 7 | {{- include "archivebrowser.labels" . | nindent 4 }} 8 | {{- if .Values.commonLabels }} 9 | {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} 10 | {{- end }} 11 | {{- if .Values.commonAnnotations }} 12 | annotations: 13 | {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} 14 | {{- end }} 15 | spec: 16 | {{- if .Values.pdb.minAvailable }} 17 | minAvailable: {{ .Values.pdb.minAvailable }} 18 | {{- end }} 19 | {{- if .Values.pdb.maxUnavailable }} 20 | maxUnavailable: {{ .Values.pdb.maxUnavailable }} 21 | {{- end }} 22 | selector: 23 | matchLabels: 24 | {{- include "archivebrowser.selectorLabels" . | nindent 6 }} 25 | {{- end }} 26 | 27 | -------------------------------------------------------------------------------- /helmchart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "archivebrowser.fullname" . }} 5 | labels: 6 | {{- include "archivebrowser.labels" . | nindent 4 }} 7 | {{- if .Values.commonLabels }} 8 | {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} 9 | {{- end }} 10 | {{- if or .Values.service.annotations .Values.commonAnnotations }} 11 | annotations: 12 | {{- if .Values.service.annotations }} 13 | {{- include "common.tplvalues.render" ( dict "value" .Values.service.annotations "context" $ ) | nindent 4 }} 14 | {{- end }} 15 | {{- if .Values.commonAnnotations }} 16 | {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} 17 | {{- end }} 18 | {{- end }} 19 | spec: 20 | type: {{ .Values.service.type }} 21 | {{- if and (eq .Values.service.type "ClusterIP") .Values.service.clusterIP }} 22 | clusterIP: {{ .Values.service.clusterIP }} 23 | {{- end }} 24 | {{- if and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerIP }} 25 | loadBalancerIP: {{ .Values.service.loadBalancerIP }} 26 | {{- end }} 27 | {{- if and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerSourceRanges }} 28 | loadBalancerSourceRanges: 29 | {{- toYaml .Values.service.loadBalancerSourceRanges | nindent 4 }} 30 | {{- end }} 31 | {{- if or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort") }} 32 | externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} 33 | {{- end }} 34 | ports: 35 | - port: {{ .Values.service.port }} 36 | targetPort: http 37 | protocol: TCP 38 | name: http 39 | {{- if and (or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort")) .Values.service.nodePort }} 40 | nodePort: {{ .Values.service.nodePort }} 41 | {{- end }} 42 | selector: 43 | {{- include "archivebrowser.selectorLabels" . | nindent 4 }} 44 | 45 | -------------------------------------------------------------------------------- /helmchart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for archivebrowser. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | # Override Kubernetes version 6 | kubeVersion: '' 7 | 8 | imagePullSecrets: [] 9 | nameOverride: '' 10 | fullnameOverride: '' 11 | 12 | # Annotations to add to all deployed objects 13 | commonAnnotations: {} 14 | 15 | # Labels to add to all deployed objects 16 | commonLabels: {} 17 | 18 | replicaCount: 1 19 | 20 | image: 21 | repository: registry.gitlab.com/aonahara/archive-browser 22 | tag: latest 23 | pullPolicy: Always 24 | 25 | pdb: 26 | # Specifies whether a pod disruption budget should be created 27 | create: false 28 | 29 | # Minimum number/percentage of pods that should remain scheduled 30 | minAvailable: 1 31 | 32 | # Maximum number/percentage of pods that may be made unavailable 33 | # maxUnavailable: 1 34 | 35 | # Additional pod annotations 36 | podAnnotations: {} 37 | 38 | # Additional pod labels 39 | podLabels: {} 40 | 41 | livenessProbe: 42 | # Enable liveness probe 43 | enabled: true 44 | 45 | # Delay before the liveness probe is initiated 46 | initialDelaySeconds: 0 47 | 48 | # How often to perform the liveness probe 49 | periodSeconds: 10 50 | 51 | # When the liveness probe times out 52 | timeoutSeconds: 1 53 | 54 | # Minimum consecutive failures for the liveness probe to be considered failed after having succeeded 55 | failureThreshold: 3 56 | 57 | # Minimum consecutive successes for the liveness probe to be considered successful after having failed 58 | successThreshold: 1 59 | 60 | readinessProbe: 61 | # Enable readiness probe 62 | enabled: true 63 | 64 | # Delay before the readiness probe is initiated 65 | initialDelaySeconds: 0 66 | 67 | # How often to perform the readiness probe 68 | periodSeconds: 10 69 | 70 | # When the readiness probe times out 71 | timeoutSeconds: 1 72 | 73 | # Minimum consecutive failures for the readiness probe to be considered failed after having succeeded 74 | failureThreshold: 3 75 | 76 | # Minimum consecutive successes for the readiness probe to be considered successful after having failed 77 | successThreshold: 1 78 | 79 | service: 80 | # Service annotations 81 | annotations: {} 82 | 83 | # Service type 84 | type: ClusterIP 85 | 86 | # Static cluster IP address or None for headless service when service type is ClusterIP 87 | # clusterIP: 10.43.0.100 88 | 89 | # Static load balancer IP address when service type is LoadBalancer 90 | # loadBalancerIP: 10.0.0.100 91 | 92 | # Source IP address ranges when service type is LoadBalancer 93 | # loadBalancerSourceRanges: 94 | # - 10.0.0.0/24 95 | 96 | # External traffic routing policy when service type is LoadBalancer or NodePort 97 | externalTrafficPolicy: Cluster 98 | 99 | # Service port 100 | port: 3000 101 | 102 | # Service node port when service type is LoadBalancer or NodePort 103 | # nodePort: 30000 104 | 105 | ingress: 106 | enabled: false 107 | 108 | # IngressClass that will be be used to implement the Ingress 109 | ingressClassName: '' 110 | 111 | # Ingress path type 112 | pathType: ImplementationSpecific 113 | 114 | annotations: 115 | {} 116 | # kubernetes.io/ingress.class: nginx 117 | # kubernetes.io/tls-acme: "true" 118 | hosts: 119 | # - host: archive-browser.local 120 | # paths: [] 121 | tls: [] 122 | # - secretName: archive-browser-tls 123 | # hosts: 124 | # - archive-browser.local 125 | 126 | resources: 127 | {} 128 | # We usually recommend not to specify default resources and to leave this as a conscious 129 | # choice for the user. This also increases chances charts run on environments with little 130 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 131 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 132 | # limits: 133 | # cpu: 100m 134 | # memory: 128Mi 135 | # requests: 136 | # cpu: 100m 137 | # memory: 128Mi 138 | 139 | nodeSelector: {} 140 | 141 | tolerations: [] 142 | 143 | affinity: {} 144 | 145 | # Additional container environment variables 146 | extraEnvVars: [] 147 | # - name: MY-NAME 148 | # value: "MY-VALUE" 149 | 150 | # Name of existing ConfigMap containing additional container environment variables 151 | extraEnvVarsCM: 152 | 153 | # Name of existing Secret containing additional container environment variables 154 | extraEnvVarsSecret: 155 | -------------------------------------------------------------------------------- /helmfile.yaml: -------------------------------------------------------------------------------- 1 | helmDefaults: 2 | createNamespace: false 3 | atomic: true 4 | wait: true 5 | 6 | releases: 7 | - name: archive-browser 8 | chart: ./helmchart 9 | version: v0.0.1 10 | installed: true 11 | values: 12 | - values.prod.yaml 13 | -------------------------------------------------------------------------------- /modules/AboutPage.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import PageBase from './shared/PageBase'; 3 | import { SITE_NAME } from './shared/config'; 4 | 5 | const AboutPage = () => { 6 | return ( 7 | 8 | 9 | About - {SITE_NAME} 10 | 11 |
12 |

Welcome to the archives

13 |

14 | This website aims to back up as much Virtual YouTuber content as 15 | possible, mainly from Hololive. We back up (almost) everything 16 | available on the YouTube video page, including the thumbnail, 17 | description, chat logs, and captions (if available). All videos are 18 | archived at the highest quality available. 19 |

20 |

21 | We only back up publicly available content. This means you won't find 22 | unarchived streams and members-only content here. 23 |

24 |

25 | This project started on February 20th, 2021. Any videos that are 26 | already gone by that date won't be available here. We're currently not 27 | accepting video files for content that's already gone. 28 |

29 |

30 | If you have any questions or concerns, feel free to hop on{' '} 31 | 36 | our Discord server 37 | 38 | . You can also contact me on Twitter at{' '} 39 | 44 | @kitsune_cw 45 | 46 | . For legal inquiries, feel free to contact me through the channels 47 | above, or send me an email at{' '} 48 | 53 | kitsune@ragtag.moe 54 | 55 | . 56 |

57 |

58 | This project is open source! Forking the project and running your own 59 | instance is highly encouraged. Check out the source code{' '} 60 | 65 | here 66 | 67 | . 68 |

69 |

Enjoy!

70 |
71 |
72 | ); 73 | }; 74 | 75 | export default AboutPage; 76 | -------------------------------------------------------------------------------- /modules/ApiDocsPage.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { ApiSearchSortFields } from '../pages/api/v1/search'; 3 | import PageBase from './shared/PageBase'; 4 | import { SITE_NAME, SITE_URL } from './shared/config'; 5 | 6 | const ApiDocsPage = () => { 7 | return ( 8 | 9 | 10 | API - {SITE_NAME} 11 | 12 |
13 |

API Documentation

14 |

15 | We provide a publicly accessible API to query the data available in 16 | our archive. The following is a summary of the available endpoints. 17 | For more details, check out{' '} 18 | 23 | the source code 24 | 25 | . 26 |

27 |

Searching

28 |

Basic Search

29 |
 30 |           GET /api/v1/search
 31 |         
32 |

Query parameters

33 | 34 | 35 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 57 | 67 | 68 | 69 | 72 | 75 | 76 | 77 | 78 | 81 | 82 | 83 | 84 | 87 | 88 | 89 |
36 | q 37 | search query, just like using the search bar
42 | v 43 | find one video with the given video id
48 | channel_id 49 | find videos that belong to the given channel id
55 | sort 56 | 58 | sort field, can be one of 59 |
    60 | {ApiSearchSortFields.map((field) => ( 61 |
  • 62 | {field} 63 |
  • 64 | ))} 65 |
66 |
70 | sort_order 71 | 73 | either asc or desc 74 |
79 | from 80 | offset results for pagination
85 | size 86 | number of results to show in one page
90 |

Example

91 |
    92 |
  • 93 |

    94 | Search for haachama, and sort results by video 95 | duration from longest to shortest. 96 |

    97 |
     98 |               
     99 |                 {`curl ${SITE_URL}api/v1/search?q=haachama&sort=duration&sort_order=desc`}
    100 |               
    101 |             
    102 |
  • 103 |
104 |

Embedding videos

105 |

106 | Videos from this website can be embedded with the URL{' '} 107 | /embed/:videoId. For example, 108 |

109 |
110 |           {`
111 |