feat: initial commit
This commit is contained in:
commit
392555d587
13 changed files with 428 additions and 0 deletions
22
.drone.yml
Normal file
22
.drone.yml
Normal file
|
@ -0,0 +1,22 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: Build
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Build and deploy site
|
||||
image: nixos/nix
|
||||
commands:
|
||||
- nix --experimental-features 'nix-command flakes' build
|
||||
- cd result/ && nix --experimental-features 'nix-command flakes' run nixpkgs#zip -- -r $DRONE_WORKSPACE/deploy.zip * && cd ..
|
||||
- 'nix --experimental-features "nix-command flakes" run nixpkgs#curl -- --fail --upload-file deploy.zip -H "Authorization: token $GITEA_TOKEN" https://git.ashhhleyyy.dev/api/packages/ash/generic/wfm/$DRONE_COMMIT_SHA/wfm.zip'
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: GITEA_TOKEN
|
||||
|
||||
trigger:
|
||||
event: [push]
|
||||
branch: [main]
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/videos/
|
66
css/main.css
Normal file
66
css/main.css
Normal file
|
@ -0,0 +1,66 @@
|
|||
html, body {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.plyr {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.video-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.video-list li {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.video-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 320px;
|
||||
font-weight: normal
|
||||
}
|
||||
|
||||
.video-card h2 {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
margin: 4px;
|
||||
/* opacity: 0; */
|
||||
}
|
||||
|
||||
/* .video-card:hover h2 {
|
||||
opacity: 1;
|
||||
} */
|
||||
|
||||
.thumbnail-container {
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.thumbnail-duration {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 4px;
|
||||
color: white;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(0, 0, 0, .45);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.video-card:hover .thumbnail-duration {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
max-width: 320px;
|
||||
border-radius: 8px;
|
||||
}
|
43
flake.lock
Normal file
43
flake.lock
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1671268780,
|
||||
"narHash": "sha256-9Okbivo10bcXEGCtmAQNfJt1Zpk6B3tjkSQ2CIXmTCg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "80c24eeb9ff46aa99617844d0c4168659e35175f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
30
flake.nix
Normal file
30
flake.nix
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem(system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
inherit (pkgs) lib;
|
||||
in
|
||||
{
|
||||
packages.default = pkgs.stdenv.mkDerivation rec {
|
||||
name = "wfm";
|
||||
src = ./.;
|
||||
phases = "installPhase";
|
||||
installPhase = ''
|
||||
mkdir -p $out/
|
||||
cp -r $src/css/ $out/
|
||||
cp -r $src/js/ $out/
|
||||
cp -r $src/vendor/ $out/
|
||||
cp $src/index.html $out/index.html
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
45
index-media.sh
Executable file
45
index-media.sh
Executable file
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
shopt -s nullglob
|
||||
|
||||
# I have I feeling I will hate this script by the time I finish writing it
|
||||
|
||||
# Some cursed JSON header
|
||||
echo '{'
|
||||
echo '"videos":['
|
||||
|
||||
isfirst="1"
|
||||
|
||||
for filename in *.mp4; do
|
||||
if [ -f "$filename" ]; then
|
||||
if [ $isfirst -ne "1" ]; then
|
||||
echo ','
|
||||
fi
|
||||
isfirst="0"
|
||||
name="${filename%.mp4}"
|
||||
echo '{'
|
||||
echo '"filename":"'"$filename"'",'
|
||||
echo '"subtitles":['
|
||||
sub_isfirst="1"
|
||||
IFS=$'\n'
|
||||
for subtitleName in "$name".*.vtt; do
|
||||
if [ $sub_isfirst -ne 1 ]; then
|
||||
echo ','
|
||||
fi
|
||||
echo '"'$subtitleName'"'
|
||||
sub_isfirst="0"
|
||||
done
|
||||
echo '],'
|
||||
ffmpeg -n -ss 00:00:01.00 -i "$filename" -vf 'scale=320:320:force_original_aspect_ratio=decrease' -vframes 1 "$name.jpg"
|
||||
echo '"thumbnail":"'"$name"'.jpg",'
|
||||
dimensions=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "$filename")
|
||||
echo '"dimensions":"'"$dimensions"'",'
|
||||
duration=$(ffprobe -v error -sexagesimal -select_streams v:0 -show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 "$filename")
|
||||
echo '"duration":"'"$duration"'"'
|
||||
echo '}'
|
||||
fi
|
||||
done
|
||||
|
||||
# Close everything off :)
|
||||
echo ']'
|
||||
echo '}'
|
15
index.html
Normal file
15
index.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<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">
|
||||
<title>Watch</title>
|
||||
<link rel="stylesheet" href="vendor/plyr-3.7.3.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<script type="module" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
181
js/main.js
Normal file
181
js/main.js
Normal file
|
@ -0,0 +1,181 @@
|
|||
import Plyr from '../vendor/plyr-3.7.3.mjs';
|
||||
|
||||
const container = document.getElementById('container');
|
||||
|
||||
function displayError(message) {
|
||||
const errorBox = document.createElement('div');
|
||||
errorBox.classList.add('error');
|
||||
errorBox.innerText = message;
|
||||
container.appendChild(errorBox);
|
||||
throw new Error(); // abort
|
||||
}
|
||||
|
||||
function loadPlaybackState(videoUrl) {
|
||||
const item = localStorage.getItem('wfm__playerstate');
|
||||
if (item) {
|
||||
const data = JSON.parse(item);
|
||||
if (Object.prototype.hasOwnProperty.call(data, videoUrl)) {
|
||||
return data[videoUrl];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
time: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function savePlaybackState(videoUrl, state) {
|
||||
const item = localStorage.getItem('wfm__playerstate');
|
||||
let data;
|
||||
if (item) {
|
||||
data = JSON.parse(item);
|
||||
} else {
|
||||
data = {};
|
||||
}
|
||||
data[videoUrl] = state;
|
||||
localStorage.setItem('wfm__playerstate', JSON.stringify(data));
|
||||
}
|
||||
|
||||
function parseQuery() {
|
||||
if (window.location.hash.length > 1) {
|
||||
const qs = window.location.hash.substring(1);
|
||||
const query = new URLSearchParams(qs);
|
||||
const source = query.get('source');
|
||||
const sourceType = query.get('source_type');
|
||||
const captions = query.getAll('captions');
|
||||
const captionLanguages = query.getAll('caption_langs');
|
||||
const captionLabels = query.getAll('caption_labels');
|
||||
if (!source || !sourceType) {
|
||||
displayError('missing source or sourceType parameter');
|
||||
}
|
||||
if (captions.length !== captionLanguages.length || captions.length !== captionLabels.length) {
|
||||
displayError('mismatch between captions, caption_langs and caption_labels length');
|
||||
}
|
||||
return {
|
||||
source, sourceType,
|
||||
captions, captionLanguages, captionLabels,
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseTimestamp(timestamp) {
|
||||
const results = /^(0|[1-9]+):([0-9]{2}):([0-9]{2}).([0-9]+)$/g.exec(timestamp);
|
||||
const [_, hours, mins, secs, frac] = results;
|
||||
return {
|
||||
hours, mins, secs, frac,
|
||||
};
|
||||
}
|
||||
|
||||
function formatTimestamp({ hours, mins, secs, frac }) {
|
||||
const ms = `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
if (hours !== 0) {
|
||||
return `${hours}:${ms}`;
|
||||
} else {
|
||||
return ms;
|
||||
}
|
||||
}
|
||||
|
||||
const query = parseQuery();
|
||||
|
||||
if (query !== null) {
|
||||
const {
|
||||
source, sourceType,
|
||||
captions, captionLanguages, captionLabels
|
||||
} = query;
|
||||
const video = document.createElement('video');
|
||||
video.controls = true;
|
||||
video.crossOrigin = 'anonymous';
|
||||
|
||||
const sourceEle = document.createElement('source');
|
||||
sourceEle.src = source;
|
||||
sourceEle.type = sourceType;
|
||||
video.appendChild(sourceEle);
|
||||
|
||||
for (let i = 0; i < captions.length; i++) {
|
||||
const url = captions[i];
|
||||
const lang = captionLanguages[i];
|
||||
const label = captionLabels[i];
|
||||
const track = document.createElement('track');
|
||||
track.kind = 'subtitles';
|
||||
track.label = label;
|
||||
track.srclang = lang;
|
||||
track.src = url;
|
||||
video.appendChild(track);
|
||||
}
|
||||
|
||||
video.autoplay = true;
|
||||
|
||||
container.appendChild(video);
|
||||
|
||||
const player = new Plyr(video, {
|
||||
captions: {
|
||||
active: true,
|
||||
},
|
||||
iconUrl: 'vendor/plyr-3.7.3.svg',
|
||||
});
|
||||
|
||||
player.on('ready', () => {
|
||||
const state = loadPlaybackState(source);
|
||||
console.log('setting start time', state);
|
||||
player.currentTime = state.time;
|
||||
console.log(player.currentTime);
|
||||
});
|
||||
|
||||
player.on('timeupdate', () => {
|
||||
savePlaybackState(source, {
|
||||
time: player.currentTime,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const BASE = '/videos/';
|
||||
|
||||
fetch(BASE+'media.json')
|
||||
.then(res => res.json())
|
||||
.then(({ videos }) => {
|
||||
const ul = document.createElement('ul');
|
||||
ul.classList.add('video-list');
|
||||
for (const video of videos) {
|
||||
//if (!video.filename.includes('telescope')) continue;
|
||||
const query = [
|
||||
['source', BASE+video.filename],
|
||||
['source_type', 'video/mp4'],
|
||||
...video.subtitles.map(subName => ['captions', BASE+subName]),
|
||||
...video.subtitles.map(subName => {
|
||||
const s = subName.split('.');
|
||||
return ['caption_langs', s[s.length - 2]];
|
||||
}),
|
||||
...video.subtitles.map(subName => ['caption_labels', subName]),
|
||||
];
|
||||
const duration = parseTimestamp(video.duration);
|
||||
const thumbnail = video.thumbnail;
|
||||
const thumbnailContainer = document.createElement('div');
|
||||
thumbnailContainer.classList.add('thumbnail-container');
|
||||
const durationEle = document.createElement('p');
|
||||
durationEle.classList.add('thumbnail-duration');
|
||||
durationEle.innerText = formatTimestamp(duration);
|
||||
const img = document.createElement('img');
|
||||
img.src = BASE+thumbnail;
|
||||
img.classList.add('thumbnail');
|
||||
const qs = new URLSearchParams(query).toString();
|
||||
const ele = document.createElement('li');
|
||||
const a = document.createElement('a');
|
||||
a.classList.add('video-card');
|
||||
a.href = '#' + qs;
|
||||
const title = document.createElement('h2');
|
||||
title.innerText = video.filename;
|
||||
thumbnailContainer.appendChild(img);
|
||||
thumbnailContainer.appendChild(durationEle);
|
||||
a.appendChild(thumbnailContainer);
|
||||
a.appendChild(title);
|
||||
ele.appendChild(a);
|
||||
ul.appendChild(ele);
|
||||
}
|
||||
container.appendChild(ul);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', (e) => {
|
||||
window.location.reload();
|
||||
});
|
1
result
Symbolic link
1
result
Symbolic link
|
@ -0,0 +1 @@
|
|||
/nix/store/dhcsck44wqr3jr3ir38r6a15rgsmlhfx-wfm
|
21
vendor/plyr-3.7.3.LICENSE.md
vendored
Normal file
21
vendor/plyr-3.7.3.LICENSE.md
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Sam Potts
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
1
vendor/plyr-3.7.3.css
vendored
Normal file
1
vendor/plyr-3.7.3.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendor/plyr-3.7.3.mjs
vendored
Normal file
1
vendor/plyr-3.7.3.mjs
vendored
Normal file
File diff suppressed because one or more lines are too long
1
vendor/plyr-3.7.3.svg
vendored
Normal file
1
vendor/plyr-3.7.3.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.6 KiB |
Loading…
Reference in a new issue