progress
ci / build (push) Has been cancelled
Details
ci / build (push) Has been cancelled
Details
This commit is contained in:
parent
11032ee83c
commit
d4b409fe67
|
@ -4,6 +4,8 @@ pnpm required for workspaces.
|
|||
|
||||
Kubernetes for Development using Tiltfile
|
||||
|
||||
kubefwd and entr for DNS in dev cluster
|
||||
|
||||
dokku for Production, deployed with `git push`.
|
||||
|
||||
(dokku is slowly being replaced by Kubernetes)
|
||||
|
|
22
Makefile
22
Makefile
|
@ -30,6 +30,17 @@ tilt:
|
|||
tilt up
|
||||
|
||||
secrets:
|
||||
|
||||
kubectl --namespace futureporn delete secret scout --ignore-not-found
|
||||
kubectl --namespace futureporn create secret generic scout \
|
||||
--from-literal=recentsToken=${SCOUT_RECENTS_TOKEN} \
|
||||
--from-literal=strapiApiKey=${SCOUT_STRAPI_API_KEY} \
|
||||
--from-literal=imapServer=${SCOUT_IMAP_SERVER} \
|
||||
--from-literal=imapPort=${SCOUT_IMAP_PORT} \
|
||||
--from-literal=imapUsername=${SCOUT_IMAP_USERNAME} \
|
||||
--from-literal=imapPassword=${SCOUT_IMAP_PASSWORD} \
|
||||
--from-literal=imapAccessToken=${SCOUT_IMAP_ACCESS_TOKEN} \
|
||||
|
||||
kubectl --namespace futureporn delete secret link2cid --ignore-not-found
|
||||
kubectl --namespace futureporn create secret generic link2cid \
|
||||
--from-literal=apiKey=${LINK2CID_API_KEY}
|
||||
|
@ -38,7 +49,6 @@ secrets:
|
|||
kubectl --namespace cert-manager create secret generic vultr \
|
||||
--from-literal=apiKey=${VULTR_API_KEY}
|
||||
|
||||
|
||||
kubectl --namespace futureporn delete secret vultr --ignore-not-found
|
||||
kubectl --namespace futureporn create secret generic vultr \
|
||||
--from-literal=containerRegistryUsername=${VULTR_CONTAINER_REGISTRY_USERNAME} \
|
||||
|
@ -58,7 +68,7 @@ secrets:
|
|||
--from-literal=adminJwtSecret=${STRAPI_ADMIN_JWT_SECRET} \
|
||||
--from-literal=apiTokenSalt=${STRAPI_API_TOKEN_SALT} \
|
||||
--from-literal=appKeys=${STRAPI_APP_KEYS} \
|
||||
--from-literal=databaseUrl=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} \
|
||||
--from-literal=databaseUrl=postgres.futureporn.svc.cluster.local://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} \
|
||||
--from-literal=jwtSecret=${STRAPI_JWT_SECRET} \
|
||||
--from-literal=muxPlaybackRestrictionId=${MUX_PLAYBACK_RESTRICTION_ID} \
|
||||
--from-literal=muxSigningKeyPrivateKey=${MUX_SIGNING_KEY_PRIVATE_KEY} \
|
||||
|
@ -75,14 +85,6 @@ secrets:
|
|||
|
||||
|
||||
|
||||
|
||||
# --from-literal=sessionSecret=$(SESSION_SECRET) \
|
||||
# --from-literal=twitchClientId=$(TWITCH_CLIENT_ID) \
|
||||
# --from-literal=twitchClientSecret=$(TWITCH_CLIENT_SECRET) \
|
||||
# --from-literal=gumroadClientId=$(GUMROAD_CLIENT_ID) \
|
||||
# --from-literal=gumroadClientSecret=$(GUMROAD_CLIENT_SECRET)
|
||||
|
||||
|
||||
define _script
|
||||
cat <<'EOF' | ctlptl apply -f -
|
||||
apiVersion: ctlptl.dev/v1alpha1
|
||||
|
|
|
@ -24,7 +24,7 @@ spec:
|
|||
kind: HelmRepository
|
||||
name: bitnami
|
||||
values:
|
||||
fullnameOverride: windmill-postgresql-cool
|
||||
fullnameOverride: windmill-postgresql-uncool
|
||||
postgresql:
|
||||
auth:
|
||||
postgresPassword: windmill
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: GitRepository
|
||||
metadata:
|
||||
name: chisel-operator
|
||||
namespace: futureporn
|
||||
spec:
|
||||
interval: 5m
|
||||
url: https://github.com/FyraLabs/chisel-operator
|
||||
ref:
|
||||
branch: master
|
||||
---
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: chisel-operator
|
||||
namespace: futureporn
|
||||
spec:
|
||||
interval: 10m
|
||||
targetNamespace: futureporn
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: chisel-operator
|
||||
path: "./kustomize"
|
||||
prune: true
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: futureporn
|
||||
resources:
|
||||
- chisel.yaml
|
|
@ -0,0 +1,37 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: capture
|
||||
namespace: futureporn
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: capture
|
||||
ports:
|
||||
- name: capture
|
||||
port: 80
|
||||
targetPort: 5566
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: capture
|
||||
namespace: futureporn
|
||||
labels:
|
||||
app: capture
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: capture
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: capture
|
||||
spec:
|
||||
containers:
|
||||
- name: capture
|
||||
image: "{{ .Values.capture.containerName }}"
|
||||
ports:
|
||||
- containerPort: 5566
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: echo-deployment
|
||||
namespace: futureporn
|
||||
labels:
|
||||
app.kubernetes.io/name: echo
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: echo-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: echo-server
|
||||
spec:
|
||||
containers:
|
||||
- name: echo-server
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
image: jmalloc/echo-server
|
||||
ports:
|
||||
- name: http-port
|
||||
containerPort: 8080
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: echo
|
||||
namespace: futureporn
|
||||
labels:
|
||||
app.kubernetes.io/name: echo
|
||||
spec:
|
||||
ports:
|
||||
- name: http-port
|
||||
port: 8080
|
||||
targetPort: http-port
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: echo-server
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ngrok
|
||||
namespace: futureporn
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: ngrok
|
||||
k8s.ngrok.com/namespace: futureporn
|
||||
k8s.ngrok.com/service: ngrok
|
||||
spec:
|
||||
ingressClassName: ngrok
|
||||
tls:
|
||||
- secretName: ngrok-tls
|
||||
hosts:
|
||||
- "{{ .Values.ngrok.hostname }}"
|
||||
rules:
|
||||
- host: "{{ .Values.ngrok.hostname }}"
|
||||
http:
|
||||
paths:
|
||||
- path: /echo
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: echo
|
||||
port:
|
||||
number: 8080
|
||||
- path: /game
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: game-2048
|
||||
port:
|
||||
number: 8080
|
||||
# - path: /strapi
|
||||
# pathType: Prefix
|
||||
# backend:
|
||||
# service:
|
||||
# name: strapi
|
||||
# port:
|
||||
# number: 1337
|
||||
# - path: /next
|
||||
# pathType: Prefix
|
||||
# backend:
|
||||
# service:
|
||||
# name: next
|
||||
# port:
|
||||
# number: 3000
|
||||
|
||||
# ---
|
||||
# apiVersion: networking.k8s.io/v1
|
||||
# kind: Ingress
|
||||
# metadata:
|
||||
# name: echo-ing
|
||||
# namespace: futureporn
|
||||
# annotations:
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# cert-manager.io/cluster-issuer: letsencrypt-staging
|
||||
# spec:
|
||||
# backend:
|
||||
# serviceName: echo-service
|
||||
# servicePort: 8080
|
||||
# tls:
|
||||
# - secretName: next-tls
|
||||
# hosts:
|
||||
# - echo.test
|
||||
# rules:
|
||||
# - host: echo.test
|
||||
# http:
|
||||
# paths:
|
||||
# - path: /
|
||||
# pathType: Prefix
|
||||
# backend:
|
||||
# service:
|
||||
# name: echo-service
|
||||
# port:
|
||||
# number: 8080
|
|
@ -1,3 +1,6 @@
|
|||
{{ if eq .Values.managedBy "Helm" }}
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
|
@ -60,3 +63,5 @@ spec:
|
|||
secretKeyRef:
|
||||
name: vultr
|
||||
key: apiKey
|
||||
|
||||
{{ end }}
|
|
@ -0,0 +1,34 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: game-2048
|
||||
namespace: futureporn
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: game-2048
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: game-2048
|
||||
namespace: futureporn
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: game-2048
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: game-2048
|
||||
spec:
|
||||
containers:
|
||||
- name: backend
|
||||
image: mendhak/http-https-echo
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
|
@ -0,0 +1,108 @@
|
|||
---
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
metadata:
|
||||
name: traefik-ingress-controller
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- services
|
||||
- endpoints
|
||||
- secrets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- ingresses/status
|
||||
verbs:
|
||||
- update
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
metadata:
|
||||
name: traefik-ingress-controller
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: traefik-ingress-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: traefik-ingress-controller
|
||||
namespace: kube-system
|
||||
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: traefik-ingress-controller
|
||||
namespace: kube-system
|
||||
---
|
||||
kind: DaemonSet
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
name: traefik-ingress-controller
|
||||
namespace: kube-system
|
||||
labels:
|
||||
k8s-app: traefik-ingress-lb
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s-app: traefik-ingress-lb
|
||||
name: traefik-ingress-lb
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: traefik-ingress-lb
|
||||
name: traefik-ingress-lb
|
||||
spec:
|
||||
serviceAccountName: traefik-ingress-controller
|
||||
terminationGracePeriodSeconds: 60
|
||||
containers:
|
||||
- image: traefik:v1.7
|
||||
name: traefik-ingress-lb
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
hostPort: 80
|
||||
- name: admin
|
||||
containerPort: 8080
|
||||
hostPort: 8080
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- NET_BIND_SERVICE
|
||||
args:
|
||||
- --api
|
||||
- --kubernetes
|
||||
- --logLevel=INFO
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: traefik-ingress-service
|
||||
namespace: kube-system
|
||||
spec:
|
||||
selector:
|
||||
k8s-app: traefik-ingress-lb
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
name: web
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
name: admin
|
|
@ -0,0 +1,70 @@
|
|||
{{ if eq .Values.managedBy "Helm" }}
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: ipfs-pod
|
||||
namespace: default
|
||||
labels:
|
||||
app.kubernetes.io/name: ipfs
|
||||
spec:
|
||||
containers:
|
||||
- name: ipfs
|
||||
image: ipfs/kubo
|
||||
ports:
|
||||
- containerPort: 5001
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: ipfs-pvc
|
||||
mountPath: /data/ipfs
|
||||
restartPolicy: OnFailure
|
||||
volumes:
|
||||
- name: ipfs-pvc
|
||||
persistentVolumeClaim:
|
||||
claimName: ipfs-pvc
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: ipfs
|
||||
namespace: default
|
||||
annotations:
|
||||
meta.helm.sh/release-name: fp
|
||||
meta.helm.sh/release-namespace: default
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: {{ .Values.managedBy }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 40Gi
|
||||
storageClassName: {{ .Values.storageClassName }}
|
||||
|
||||
|
||||
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ipfs-service
|
||||
namespace: default
|
||||
annotations:
|
||||
meta.helm.sh/release-name: fp
|
||||
meta.helm.sh/release-namespace: default
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: {{ .Values.managedBy }}
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: ipfs
|
||||
ports:
|
||||
- name: gateway
|
||||
protocol: TCP
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
- name: api
|
||||
protocol: TCP
|
||||
port: 5001
|
||||
targetPort: 5001
|
||||
|
||||
{{ end }}
|
|
@ -2,7 +2,7 @@ apiVersion: v1
|
|||
kind: Service
|
||||
metadata:
|
||||
name: link2cid
|
||||
namespace: default
|
||||
namespace: futureporn
|
||||
spec:
|
||||
selector:
|
||||
app: link2cid
|
||||
|
@ -22,7 +22,7 @@ apiVersion: apps/v1
|
|||
kind: Deployment
|
||||
metadata:
|
||||
name: link2cid
|
||||
namespace: default
|
||||
namespace: futureporn
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
|
@ -54,10 +54,10 @@ apiVersion: v1
|
|||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: link2cid
|
||||
namespace: default
|
||||
namespace: futureporn
|
||||
annotations:
|
||||
meta.helm.sh/release-name: fp
|
||||
meta.helm.sh/release-namespace: default
|
||||
meta.helm.sh/release-namespace: futureporn
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: {{ .Values.managedBy }}
|
||||
spec:
|
||||
|
@ -75,7 +75,7 @@ apiVersion: networking.k8s.io/v1
|
|||
kind: Ingress
|
||||
metadata:
|
||||
name: link2cid-ingress
|
||||
namespace: default
|
||||
namespace: futureporn
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
|
@ -0,0 +1,65 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: realtime
|
||||
namespace: futureporn
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: realtime
|
||||
ports:
|
||||
- name: realtime
|
||||
port: 80
|
||||
targetPort: 5535
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: realtime
|
||||
namespace: futureporn
|
||||
labels:
|
||||
app: realtime
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: realtime
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: realtime
|
||||
spec:
|
||||
containers:
|
||||
- name: realtime
|
||||
image: "{{ .Values.realtime.containerName }}"
|
||||
ports:
|
||||
- containerPort: 5535
|
||||
|
||||
|
||||
{{ if eq .Values.managedBy "Helm" }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: realtime
|
||||
namespace: futureporn
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: letsencrypt-staging
|
||||
spec:
|
||||
tls:
|
||||
- secretName: realtime-tls
|
||||
hosts:
|
||||
- realtime.futureporn.net
|
||||
rules:
|
||||
- host: realtime.futureporn.net
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: realtime
|
||||
port:
|
||||
number: 5535
|
||||
{{ end }}
|
|
@ -0,0 +1,104 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: scout
|
||||
namespace: futureporn
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: scout
|
||||
ports:
|
||||
- name: web
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: scout
|
||||
namespace: futureporn
|
||||
labels:
|
||||
app: scout
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: scout
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: scout
|
||||
spec:
|
||||
containers:
|
||||
- name: scout
|
||||
image: "{{ .Values.scout.containerName }}"
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
env:
|
||||
- name: PUBSUB_SERVER_URL
|
||||
value: "{{ .Values.scout.pubsubServerUrl }}"
|
||||
- name: STRAPI_URL
|
||||
value: https://strapi.futureporn.svc.cluster.local
|
||||
- name: SCOUT_RECENTS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: scout
|
||||
key: recentsToken
|
||||
- name: SCOUT_IMAP_SERVER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: scout
|
||||
key: imapServer
|
||||
- name: SCOUT_IMAP_PORT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: scout
|
||||
key: imapPort
|
||||
- name: SCOUT_IMAP_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: scout
|
||||
key: imapUsername
|
||||
- name: SCOUT_IMAP_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: scout
|
||||
key: imapPassword
|
||||
- name: SCOUT_IMAP_ACCESS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: scout
|
||||
key: imapAccessToken
|
||||
- name: SCOUT_STRAPI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: scout
|
||||
key: strapiApiKey
|
||||
|
||||
|
||||
{{ if eq .Values.managedBy "Helm" }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: scout
|
||||
namespace: futureporn
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: letsencrypt-staging
|
||||
spec:
|
||||
tls:
|
||||
- secretName: scout-tls
|
||||
hosts:
|
||||
- scout.sbtp.xyz
|
||||
rules:
|
||||
- host: scout.sbtp.xyz
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: scout
|
||||
port:
|
||||
number: 3000
|
||||
{{ end }}
|
|
@ -1,22 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: ipfs-pod
|
||||
namespace: default
|
||||
labels:
|
||||
app.kubernetes.io/name: ipfs
|
||||
spec:
|
||||
containers:
|
||||
- name: ipfs
|
||||
image: ipfs/kubo
|
||||
ports:
|
||||
- containerPort: 5001
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: ipfs-pvc
|
||||
mountPath: /data/ipfs
|
||||
restartPolicy: OnFailure
|
||||
volumes:
|
||||
- name: ipfs-pvc
|
||||
persistentVolumeClaim:
|
||||
claimName: ipfs-pvc
|
|
@ -1,18 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: ipfs-pvc
|
||||
namespace: default
|
||||
annotations:
|
||||
meta.helm.sh/release-name: fp
|
||||
meta.helm.sh/release-namespace: default
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: {{ .Values.managedBy }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 40Gi
|
||||
storageClassName: {{ .Values.storageClassName }}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ipfs-service
|
||||
namespace: default
|
||||
annotations:
|
||||
meta.helm.sh/release-name: fp
|
||||
meta.helm.sh/release-namespace: default
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: {{ .Values.managedBy }}
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: ipfs
|
||||
ports:
|
||||
- name: gateway
|
||||
protocol: TCP
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
- name: api
|
||||
protocol: TCP
|
||||
port: 5001
|
||||
targetPort: 5001
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
# server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: {{ .Values.adminEmail }}
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod
|
||||
solvers:
|
||||
- dns01:
|
||||
webhook:
|
||||
groupName: acme.vultr.com
|
||||
solverName: vultr
|
||||
config:
|
||||
apiKeySecretRef:
|
||||
key: apiKey
|
||||
name: vultr
|
|
@ -1,3 +1,4 @@
|
|||
{{ if eq .Values.managedBy "Helm" }}
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
|
@ -21,3 +22,28 @@ spec:
|
|||
apiKeySecretRef:
|
||||
key: apiKey
|
||||
name: vultr-credentials
|
||||
|
||||
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
# server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: {{ .Values.adminEmail }}
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod
|
||||
solvers:
|
||||
- dns01:
|
||||
webhook:
|
||||
groupName: acme.vultr.com
|
||||
solverName: vultr
|
||||
config:
|
||||
apiKeySecretRef:
|
||||
key: apiKey
|
||||
name: vultr
|
||||
|
||||
{{ end }}
|
|
@ -0,0 +1,64 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: next
|
||||
namespace: futureporn
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: next
|
||||
ports:
|
||||
- name: web
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: next
|
||||
namespace: futureporn
|
||||
labels:
|
||||
app.kubernetes.io/name: next
|
||||
spec:
|
||||
containers:
|
||||
- name: next
|
||||
image: "{{ .Values.next.containerName }}"
|
||||
env:
|
||||
- name: HOSTNAME
|
||||
value: 0.0.0.0
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
resources: {}
|
||||
restartPolicy: OnFailure
|
||||
|
||||
|
||||
{{ if eq .Values.managedBy "Helm" }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: next
|
||||
namespace: futureporn
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: "{{ .Values.next.certIssuer }}"
|
||||
spec:
|
||||
backend:
|
||||
serviceName: next
|
||||
servicePort: 3000
|
||||
tls:
|
||||
- secretName: next-tls
|
||||
hosts:
|
||||
- "{{ .Values.next.hostname }}"
|
||||
rules:
|
||||
- host: "{{ .Values.next.hostname }}"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: next
|
||||
port:
|
||||
number: 3000
|
||||
{{ end }}
|
|
@ -0,0 +1,37 @@
|
|||
{{ if eq .Values.managedBy "tilt" }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ngrok
|
||||
namespace: futureporn
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: ngrok
|
||||
k8s.ngrok.com/namespace: futureporn
|
||||
k8s.ngrok.com/service: ngrok
|
||||
spec:
|
||||
ingressClassName: ngrok
|
||||
tls:
|
||||
- secretName: ngrok-tls
|
||||
hosts:
|
||||
- "{{ .Values.ngrok.hostname }}"
|
||||
rules:
|
||||
- host: "{{ .Values.ngrok.hostname }}"
|
||||
http:
|
||||
paths:
|
||||
- path: /next
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: next
|
||||
port:
|
||||
number: 3000
|
||||
- path: /strapi
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: strapi
|
||||
port:
|
||||
number: 1337
|
||||
{{ end }}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: pgadmin
|
||||
namespace: futureporn
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: pgadmin
|
||||
ports:
|
||||
- name: web
|
||||
protocol: TCP
|
||||
port: 5050
|
||||
targetPort: 5050
|
||||
status:
|
||||
loadBalancer: {}
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: pgadmin
|
||||
namespace: futureporn
|
||||
labels:
|
||||
app.kubernetes.io/name: pgadmin
|
||||
spec:
|
||||
containers:
|
||||
- name: pgadmin
|
||||
image: dpage/pgadmin4
|
||||
ports:
|
||||
- containerPort: 5050
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
env:
|
||||
- name: PGADMIN_LISTEN_PORT
|
||||
value: '5050'
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres
|
||||
key: password
|
||||
- name: PGADMIN_DEFAULT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: pgadmin
|
||||
key: defaultPassword
|
||||
- name: PGADMIN_DEFAULT_EMAIL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: pgadmin
|
||||
key: defaultEmail
|
||||
restartPolicy: OnFailure
|
|
@ -0,0 +1,70 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
namespace: futureporn
|
||||
name: postgres
|
||||
annotations:
|
||||
tilt.dev/down-policy: keep
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOncePod
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
resources:
|
||||
requests:
|
||||
storage: 40Gi
|
||||
storageClassName: {{ .Values.storageClassName }}
|
||||
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: futureporn
|
||||
name: postgres
|
||||
annotations:
|
||||
tilt.dev/down-policy: keep
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: postgres
|
||||
ports:
|
||||
- name: db
|
||||
protocol: TCP
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
status:
|
||||
loadBalancer: {}
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
namespace: futureporn
|
||||
name: postgres
|
||||
labels:
|
||||
app.kubernetes.io/name: postgres
|
||||
annotations:
|
||||
tilt.dev/down-policy: keep
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16.0
|
||||
env:
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgres
|
||||
key: password
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
volumeMounts:
|
||||
- name: postgres
|
||||
mountPath: /data/postgres
|
||||
restartPolicy: OnFailure
|
||||
volumes:
|
||||
- name: postgres
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres
|
|
@ -1,3 +1,5 @@
|
|||
{{ if eq .Values.managedBy "Helm" }}
|
||||
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
|
@ -23,3 +25,4 @@ roleRef:
|
|||
name: cert-manager-webhook-vultr-secret-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
||||
{{ end }}
|
|
@ -0,0 +1,180 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: strapi
|
||||
namespace: futureporn
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: strapi
|
||||
ports:
|
||||
- name: web
|
||||
port: 1337
|
||||
targetPort: 1337
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: strapi
|
||||
namespace: futureporn
|
||||
labels:
|
||||
app.kubernetes.io/name: strapi
|
||||
spec:
|
||||
containers:
|
||||
- name: strapi
|
||||
image: "{{ .Values.strapi.containerName }}"
|
||||
ports:
|
||||
- containerPort: 1337
|
||||
env:
|
||||
- name: ADMIN_JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: adminJwtSecret
|
||||
- name: API_TOKEN_SALT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: apiTokenSalt
|
||||
- name: APP_KEYS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: appKeys
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: databaseUrl
|
||||
- name: CDN_BUCKET_USC_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: cdnBucketUscUrl
|
||||
- name: DATABASE_CLIENT
|
||||
value: postgres
|
||||
- name: DATABASE_HOST
|
||||
value: postgres.futureporn.svc.cluster.local
|
||||
- name: DATABASE_NAME
|
||||
value: futureporn-strapi
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: jwtSecret
|
||||
- name: MUX_PLAYBACK_RESTRICTION_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: muxPlaybackRestrictionId
|
||||
- name: MUX_SIGNING_KEY_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: muxSigningKeyId
|
||||
- name: MUX_SIGNING_KEY_PRIVATE_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: muxSigningKeyPrivateKey
|
||||
- name: NODE_ENV
|
||||
value: production
|
||||
- name: S3_USC_BUCKET_APPLICATION_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: s3UscBucketApplicationKey
|
||||
- name: S3_USC_BUCKET_ENDPOINT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: s3UscBucketEndpoint
|
||||
- name: S3_USC_BUCKET_KEY_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: s3UscBucketKeyId
|
||||
- name: S3_USC_BUCKET_NAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: s3UscBucketName
|
||||
- name: S3_USC_BUCKET_REGION
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: s3UscBucketRegion
|
||||
- name: SENDGRID_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: sendgridApiKey
|
||||
- name: STRAPI_URL
|
||||
value: "{{ .Values.strapi.url }}"
|
||||
- name: TRANSFER_TOKEN_SALT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: strapi
|
||||
key: transferTokenSalt
|
||||
- name: PORT
|
||||
value: "{{ .Values.strapi.port }}"
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
restartPolicy: OnFailure
|
||||
|
||||
|
||||
# ---
|
||||
# apiVersion: v1
|
||||
# kind: PersistentVolumeClaim
|
||||
# metadata:
|
||||
# name: strapi
|
||||
# namespace: futureporn
|
||||
# annotations:
|
||||
# meta.helm.sh/release-name: fp
|
||||
# meta.helm.sh/release-namespace: futureporn
|
||||
# labels:
|
||||
# app.kubernetes.io/managed-by: {{ .Values.managedBy }}
|
||||
# spec:
|
||||
# accessModes:
|
||||
# - ReadWriteOnce
|
||||
# resources:
|
||||
# requests:
|
||||
# storage: 100Gi
|
||||
# storageClassName: {{ .Values.storageClassName }}
|
||||
|
||||
|
||||
{{ if eq .Values.managedBy "Helm" }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: strapi
|
||||
namespace: futureporn
|
||||
annotations:
|
||||
sbtp.xyz/managed-by: "{{ .Values.managedBy }}"
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: "{{ .Values.strapi.certIssuer }}"
|
||||
spec:
|
||||
ingressClassName: "{{ .Values.strapi.ingressClassName }}"
|
||||
backend:
|
||||
serviceName: strapi
|
||||
servicePort: 1337
|
||||
tls:
|
||||
- secretName: strapi-tls
|
||||
hosts:
|
||||
- "{{ .Values.strapi.hostname }}"
|
||||
rules:
|
||||
- host: "{{ .Values.strapi.hostname }}"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: strapi
|
||||
port:
|
||||
number: 1337
|
||||
|
||||
{{ end }}
|
|
@ -1,3 +1,5 @@
|
|||
{{ if eq .Values.managedBy "Helm" }}
|
||||
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
|
@ -26,3 +28,5 @@ spec:
|
|||
- hosts:
|
||||
- windmill2.sbtp.xyz
|
||||
secretName: windmill-tls
|
||||
|
||||
{{ end }}
|
|
@ -1,12 +1,26 @@
|
|||
# storageClassName: csi-hostpath-sc # used by minikube
|
||||
storageClassName: standard # used by Kind
|
||||
storageClassName: csi-hostpath-sc # used by minikube
|
||||
# storageClassName: standard # used by Kind
|
||||
managedBy: tilt
|
||||
link2cid:
|
||||
containerName: fp/link2cid
|
||||
next:
|
||||
containerName: fp/next
|
||||
certIssuer: letsencrypt-staging
|
||||
hostname: next.futureporn.svc.cluster.local
|
||||
capture:
|
||||
containerName: fp/capture
|
||||
scout:
|
||||
containerName: fp/scout
|
||||
pubsubServerUrl: https://realtime.futureporn.svc.cluster.local/faye
|
||||
strapi:
|
||||
containerName: fp/strapi
|
||||
port: 1337
|
||||
url: http://localhost:1337
|
||||
managedBy: Dildo
|
||||
url: https://strapi.futureporn.svc.cluster.local
|
||||
certIssuer: letsencrypt-staging
|
||||
hostname: strapi.futureporn.svc.cluster.local
|
||||
ingressClassName: ngrok
|
||||
ngrok:
|
||||
hostname: prawn-sweeping-muskrat.ngrok-free.app
|
||||
realtime:
|
||||
containerName: fp/realtime
|
||||
adminEmail: cj@futureporn.net
|
|
@ -1,12 +1,22 @@
|
|||
storageClassName: vultr-block-storage-hdd
|
||||
link2cid:
|
||||
containerName: gitea.futureporn.net/futureporn/link2cid:latest
|
||||
scout:
|
||||
containerName: gitea.futureporn.net/futureporn/scout:latest
|
||||
pubsubServerUrl: https://realtime.futureporn.net/faye
|
||||
next:
|
||||
containerName: sjc.vultrcr.com/fpcontainers/next
|
||||
containerName: gitea.futureporn.net/futureporn/next:latest
|
||||
certIssuer: letsencrypt-staging
|
||||
hostname: next.sbtp.xyz
|
||||
capture:
|
||||
containerName: gitea.futureporn.net/futureporn/capture:latest
|
||||
strapi:
|
||||
containerName: sjc.vultrcr.com/fpcontainers/strapi
|
||||
port: 1337
|
||||
url: https://portal.futureporn.net
|
||||
certIssuer: letsencrypt-prod
|
||||
hostname: strapi.sbtp.xyz
|
||||
ingressClassName: nginx
|
||||
managedBy: Helm
|
||||
adminEmail: cj@futureporn.net
|
||||
extraArgs:
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
FROM node:18-alpine
|
||||
# Install dependencies only when needed
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
# Enable `pnpm add --global` on Alpine Linux by setting
|
||||
# home location environment variable to a location already in $PATH
|
||||
# https://github.com/pnpm/pnpm/issues/784#issuecomment-1518582235
|
||||
ENV PNPM_HOME=/usr/local/bin
|
||||
|
||||
# update and install latest dependencies, add dumb-init package
|
||||
# add a non root user
|
||||
RUN apk update && apk upgrade && apk add dumb-init ffmpeg make gcc g++ python3
|
||||
|
||||
WORKDIR /app
|
||||
# Copy and install the dependencies for the project
|
||||
COPY ./packages/capture/package.json ./packages/capture/pnpm-lock.yaml ./
|
||||
|
||||
# Copy all other project files to working directory
|
||||
COPY ./packages/capture .
|
||||
# Run the next build process and generate the artifacts
|
||||
RUN pnpm i;
|
||||
|
||||
|
||||
# expose 3000 on container
|
||||
EXPOSE 3000
|
||||
|
||||
# set app host ,port and node env
|
||||
ENV HOSTNAME=0.0.0.0 PORT=3000 NODE_ENV=production
|
||||
# start the app with dumb init to spawn the Node.js runtime process
|
||||
# with signal support
|
||||
CMD [ "dumb-init", "node", "index.js" ]
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Reference-- https://pnpm.io/docker
|
||||
|
||||
FROM node:20-alpine AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
WORKDIR /app
|
||||
COPY ./packages/link2cid/package.json /app
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod
|
||||
COPY ./packages/link2cid/index.js /app
|
||||
COPY ./packages/link2cid/src /app/src
|
||||
ENTRYPOINT ["pnpm"]
|
||||
CMD ["start"]
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
## Important! Build context is the ROOT of the project.
|
||||
## this keeps the door open for future possibility of shared code between pnpm workspace packages
|
||||
|
||||
FROM node:20-slim AS base
|
||||
|
||||
FROM base AS deps
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
FROM deps AS install
|
||||
ARG NEXT_PUBLIC_SITE_URL=https://futureporn.net
|
||||
ARG NEXT_PUBLIC_STRAPI_URL=https://portal.futureporn.net
|
||||
ARG NEXT_PUBLIC_UPPY_COMPANION_URL=https://uppy.futureporn.net
|
||||
ENV NEXT_PUBLIC_SITE_URL ${NEXT_PUBLIC_SITE_URL}
|
||||
ENV NEXT_PUBLIC_STRAPI_URL ${NEXT_PUBLIC_STRAPI_URL}
|
||||
ENV NEXT_PUBLIC_UPPY_COMPANION_URL ${NEXT_PUBLIC_UPPY_COMPANION_URL}
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
COPY pnpm-lock.yaml ./
|
||||
RUN pnpm fetch
|
||||
COPY ./packages/next /app
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install
|
||||
|
||||
|
||||
FROM install AS dev
|
||||
CMD ["pnpm", "run", "dev"]
|
||||
|
||||
|
||||
|
||||
FROM install AS build
|
||||
RUN pnpm run build
|
||||
# COPY --chown=node:node --from=install /app/package.json /app/pnpm-lock.yaml ./
|
||||
# RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
# COPY --from=install /app /app # i think this is duplicate
|
||||
# can't get these to work because errors like "/prod/next/.next/standalone": not found
|
||||
# as if pnpm is not copying the build artifacts.
|
||||
# also this makes the build REALLY slow (adds ~10mins to build time)
|
||||
# RUN pnpm deploy --filter=@futureporn/next --prod /prod/next
|
||||
# RUN pnpm deploy --filter=@futureporn/link2cid --prod /prod/link2cid
|
||||
|
||||
# FROM deps as release
|
||||
# # ENV NEXT_SHARP_PATH=/app/node_modules/sharp
|
||||
# ENV NODE_ENV=production
|
||||
# WORKDIR /app
|
||||
# COPY --from=build /app/public ./public
|
||||
# COPY --from=build /app/.next/standalone ./
|
||||
# COPY --from=build /app/.next/static ./.next/static
|
||||
# CMD [ "dumb-init", "node", "server.js" ]
|
||||
|
||||
|
||||
|
||||
FROM deps AS next
|
||||
RUN apt-get update && apt-get install -y -qq --no-install-recommends dumb-init
|
||||
COPY --chown=node:node --from=build /app/package.json /app/pnpm-lock.yaml ./
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
COPY --chown=node:node --from=build /app/public ./public
|
||||
COPY --chown=node:node --from=build /app/.next/standalone ./
|
||||
COPY --chown=node:node --from=build /app/.next/static ./.next/static
|
||||
ENV TZ=UTC
|
||||
ENV NODE_ENV=production
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
CMD [ "dumb-init", "node", "server.js" ]
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN apk update
|
||||
|
||||
ENV NODE_ENV=production
|
||||
COPY pnpm-lock.yaml ./
|
||||
RUN pnpm fetch
|
||||
COPY ./packages/realtime /app
|
||||
|
||||
ENTRYPOINT ["pnpm"]
|
||||
CMD ["run", "start"]
|
|
@ -0,0 +1,17 @@
|
|||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN apk update
|
||||
|
||||
|
||||
|
||||
ENV NODE_ENV=production
|
||||
COPY pnpm-lock.yaml ./
|
||||
RUN pnpm fetch
|
||||
COPY ./packages/scout /app
|
||||
|
||||
|
||||
ENTRYPOINT ["pnpm"]
|
||||
CMD ["run", "start"]
|
|
@ -0,0 +1,34 @@
|
|||
FROM node:20-alpine as base
|
||||
WORKDIR /app
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev libc6-compat git nasm bash gcompat
|
||||
|
||||
FROM base AS install
|
||||
COPY ./packages/strapi/pnpm-lock.yaml ./packages/strapi/package.json ./
|
||||
RUN pnpm install --prod --shamefully-hoist && pnpm run build
|
||||
COPY ./packages/strapi .
|
||||
RUN chown -R node:node /app
|
||||
USER node
|
||||
|
||||
|
||||
# FROM base AS install
|
||||
# COPY ./packages/strapi/pnpm-lock.yaml ./
|
||||
# RUN pnpm fetch --prod
|
||||
# COPY ./packages/strapi .
|
||||
# RUN pnpm i --offline --prod --shamefully-hoist && pnpm run build
|
||||
# RUN chown -R node:node /app
|
||||
# USER node
|
||||
|
||||
|
||||
|
||||
FROM install AS dev
|
||||
ENV NODE_ENV=development
|
||||
ENTRYPOINT ["pnpm"]
|
||||
CMD ["run", "dev"]
|
||||
|
||||
FROM install AS release
|
||||
ENV NODE_ENV=production
|
||||
ENTRYPOINT ["pnpm"]
|
||||
CMD ["run", "start"]
|
|
@ -0,0 +1,153 @@
|
|||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
|
@ -0,0 +1,25 @@
|
|||
# Capture
|
||||
|
||||
## Dev notes
|
||||
|
||||
### youtube-dl end of stream output
|
||||
|
||||
```
|
||||
[https @ 0x5646887f1580] Opening 'https://edge11-lax.live.mmcdn.com/live-hls/amlst:hotfallingdevil-sd-fdf87e5b6c880e52d38e8c94f8ebf8728c980a91d56fb4ace13748ba59012336_trns_h264/chunklist_w881713853_b5128000_t64RlBTOjMwLjA=.m3u8' for reading
|
||||
[hls @ 0x564687dd0980] Skip ('#EXT-X-VERSION:4')
|
||||
[hls @ 0x564687dd0980] Skip ('#EXT-X-DISCONTINUITY-SEQUENCE:0')
|
||||
[hls @ 0x564687dd0980] Skip ('#EXT-X-PROGRAM-DATE-TIME:2023-01-31T17:48:45.947+00:00')
|
||||
[https @ 0x5646880bf880] Opening 'https://edge11-lax.live.mmcdn.com/live-hls/amlst:hotfallingdevil-sd-fdf87e5b6c880e52d38e8c94f8ebf8728c980a91d56fb4ace13748ba59012336_trns_h264/media_w881713853_b5128000_t64RlBTOjMwLjA=_18912.ts' for reading
|
||||
[https @ 0x564688097d00] Opening 'https://edge11-lax.live.mmcdn.com/live-hls/amlst:hotfallingdevil-sd-fdf87e5b6c880e52d38e8c94f8ebf8728c980a91d56fb4ace13748ba59012336_trns_h264/media_w881713853_b5128000_t64RlBTOjMwLjA=_18913.ts' for reading
|
||||
[https @ 0x5646887f1580] Opening 'https://edge11-lax.live.mmcdn.com/live-hls/amlst:hotfallingdevil-sd-fdf87e5b6c880e52d38e8c94f8ebf8728c980a91d56fb4ace13748ba59012336_trns_h264/chunklist_w881713853_b5128000_t64RlBTOjMwLjA=.m3u8' for reading
|
||||
[https @ 0x5646886e8580] HTTP error 403 Forbidden
|
||||
[hls @ 0x564687dd0980] keepalive request failed for 'https://edge11-lax.live.mmcdn.com/live-hls/amlst:hotfallingdevil-sd-fdf87e5b6c880e52d38e8c94f8ebf8728c980a91d56fb4ace13748ba59012336_trns_h264/chunklist_w881713853_b5128000_t64RlBTOjMwLjA=.m3u8' with error: 'Server returned 403 Forbidden (access denied)' when parsing playlist
|
||||
[https @ 0x5646886ccfc0] HTTP error 403 Forbidden
|
||||
[hls @ 0x564687dd0980] Failed to reload playlist 0
|
||||
[https @ 0x5646886bf680] HTTP error 403 Forbidden
|
||||
[hls @ 0x564687dd0980] Failed to reload playlist 0
|
||||
frame= 5355 fps= 31 q=-1.0 Lsize= 71404kB time=00:02:58.50 bitrate=3277.0kbits/s speed=1.02x
|
||||
video:68484kB audio:2790kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.181873%
|
||||
[ffmpeg] Downloaded 73117881 bytes
|
||||
[download] 100% of 69.73MiB in 02:57
|
||||
```
|
|
@ -0,0 +1,158 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// import Capture from './src/Capture.js'
|
||||
// import Video from './src/Video.js'
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import os from 'os'
|
||||
import fs from 'node:fs'
|
||||
import { loggerFactory } from "./src/logger.js"
|
||||
import { verifyStorage } from './src/disk.js'
|
||||
import faye from 'faye'
|
||||
import { record, assertDependencyDirectory, checkFFmpeg } from './src/record.js'
|
||||
import fastq from 'fastq'
|
||||
import pRetry from 'p-retry';
|
||||
import Fastify from 'fastify';
|
||||
|
||||
|
||||
|
||||
// Create a map to store the work queues
|
||||
const workQueues = new Map();
|
||||
|
||||
|
||||
|
||||
async function captureTask (args, cb) {
|
||||
const { appContext, playlistUrl, roomName } = args;
|
||||
|
||||
try {
|
||||
const downloadStream = async () => {
|
||||
const rc = await record(appContext, playlistUrl, roomName)
|
||||
if (rc !== 0) throw new Error('ffmpeg exited irregularly (return code was other than zero)')
|
||||
}
|
||||
await pRetry(downloadStream, {
|
||||
retries: 3,
|
||||
onFailedAttempt: error => {
|
||||
appContext.logger.log({ level: 'error', message: `downloadStream attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left.` });
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
// we can get here if all retries are exhausted.
|
||||
// this could be that the stream is over, the playlistUrl might be different, etc.
|
||||
// we might have queued tasks so we don't want to crash.
|
||||
appContext.logger.log({ level: 'error', message: `downloadStream exhausted all retries.` })
|
||||
appContext.logger.log({ level: 'error', message: e })
|
||||
}
|
||||
|
||||
verifyStorage(appContext)
|
||||
|
||||
appContext.logger.log({ level: 'info', message: 'Capture task complete'})
|
||||
cb(null, null)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Fastify is used to facilitate Docker health checks
|
||||
*
|
||||
*/
|
||||
async function initFastify(appContext) {
|
||||
appContext.fastify = Fastify({
|
||||
logger: true
|
||||
})
|
||||
|
||||
// Declare a route
|
||||
appContext.fastify.get('/health', function (_, reply) {
|
||||
reply.send({ message: 'futureporn-capture sneed' });
|
||||
})
|
||||
|
||||
// Run the server!
|
||||
appContext.fastify.listen({ port: appContext.env.PORT }, function (err, address) {
|
||||
if (err) {
|
||||
appContext.fastify.log.error(err)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function init () {
|
||||
|
||||
const appEnv = new Array(
|
||||
'FUTUREPORN_WORKDIR',
|
||||
'PUBSUB_SERVER_URL',
|
||||
'DOWNLOADER_UA',
|
||||
'PORT'
|
||||
)
|
||||
|
||||
const logger = loggerFactory({
|
||||
service: 'futureporn/capture'
|
||||
})
|
||||
|
||||
|
||||
const appContext = {
|
||||
env: appEnv.reduce((acc, ev) => {
|
||||
if (typeof process.env[ev] === 'undefined') throw new Error(`${ev} is undefined in env`);
|
||||
acc[ev] = process.env[ev];
|
||||
return acc;
|
||||
}, {}),
|
||||
logger,
|
||||
pkg: JSON.parse(fs.readFileSync('./package.json', { encoding: 'utf-8'})),
|
||||
workerId: `${os.hostname}-${createId()}`,
|
||||
pubsub: new faye.Client(process.env.PUBSUB_SERVER_URL)
|
||||
};
|
||||
|
||||
await initFastify(appContext);
|
||||
|
||||
assertDependencyDirectory(appContext)
|
||||
await checkFFmpeg(appContext)
|
||||
verifyStorage(appContext)
|
||||
|
||||
return appContext
|
||||
}
|
||||
|
||||
|
||||
async function main () {
|
||||
|
||||
const appContext = await init()
|
||||
|
||||
|
||||
|
||||
appContext.logger.log({ level: 'info', message: `capture version: ${appContext.pkg.version}` })
|
||||
appContext.logger.log({ level: 'info', message: `my capture directory is ${appContext.env.FUTUREPORN_WORKDIR}` })
|
||||
appContext.logger.log({ level: 'info', message: `listening for faye signals from ${appContext.env.PUBSUB_SERVER_URL}` })
|
||||
|
||||
|
||||
// connect to realtime server
|
||||
appContext.pubsub.subscribe('/signals', (message) => {
|
||||
appContext.logger.log({ level: 'debug', message: JSON.stringify(message) })
|
||||
|
||||
if (
|
||||
(message?.signal === 'start') &&
|
||||
(message?.room) &&
|
||||
(message?.url.startsWith('https://'))
|
||||
) {
|
||||
|
||||
const roomName = message.room;
|
||||
const playlistUrl = message.url;
|
||||
|
||||
// Check if a work queue for the room already exists, otherwise create a new one
|
||||
if (!workQueues.has(roomName)) {
|
||||
workQueues.set(roomName, fastq(captureTask, 1));
|
||||
}
|
||||
|
||||
|
||||
// Push the task to the corresponding work queue
|
||||
workQueues.get(roomName).push({ appContext, playlistUrl, roomName });
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "futureporn-capture",
|
||||
"version": "0.1.12",
|
||||
"main": "index.js",
|
||||
"license": "Unlicense",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node --trace-warnings index",
|
||||
"test": "FUTUREPORN_WORKDIR=/home/chris/Downloads mocha",
|
||||
"integration": "FUTUREPORN_WORKDIR=/home/chris/Downloads mocha ./integration/**/*.test.js",
|
||||
"dev": "FUTUREPORN_WORKDIR=/home/chris/Downloads nodemon index"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paralleldrive/cuid2": "^2.1.8",
|
||||
"diskusage": "^1.1.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"execa": "^6.1.0",
|
||||
"fastify": "^4.12.0",
|
||||
"fastq": "^1.15.0",
|
||||
"faye": "^1.4.0",
|
||||
"faye-websocket": "^0.11.4",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"https": "^1.0.0",
|
||||
"ioredis": "^5.2.4",
|
||||
"minimatch": "^5.1.1",
|
||||
"p-retry": "^5.1.2",
|
||||
"postgres": "^3.3.3",
|
||||
"rxjs": "^7.8.0",
|
||||
"sql": "^0.78.0",
|
||||
"winston": "^3.9.0",
|
||||
"youtube-dl-wrap": "git+https://github.com/insanity54/youtube-dl-wrap.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.7",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"mocha": "^10.2.0",
|
||||
"multiformats": "^11.0.1",
|
||||
"node-abort-controller": "^3.0.1",
|
||||
"node-fetch": "^3.3.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"sinon": "^15.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-test": "^3.1.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
|
||||
import Voddo from './Voddo.js'
|
||||
import {loggerFactory} from 'common/logger'
|
||||
|
||||
const logger = loggerFactory({
|
||||
service: 'futureporn/capture'
|
||||
})
|
||||
|
||||
export default class Capture {
|
||||
|
||||
constructor(opts) {
|
||||
this.date = opts?.date
|
||||
this.sql = opts.sql
|
||||
this.ipfs = opts.ipfs
|
||||
this.idleTimeout = opts.idleTimeout || 1000*60*15
|
||||
this.video = opts.video
|
||||
this.voddo = opts.voddo
|
||||
this.workerId = opts.workerId
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* upload VOD to ipfs
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves {String} cid
|
||||
*/
|
||||
async upload (filename) {
|
||||
const cid = await this.ipfs.upload(filename)
|
||||
return cid
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* save Vod data to db
|
||||
*/
|
||||
async save (cid, timestamp) {
|
||||
logger.log({ level: 'debug', message: `saving ${cid} \n w/ captureDate ${timestamp}` })
|
||||
this.date = timestamp
|
||||
return await this.sql`INSERT INTO vod ( "videoSrcHash", "captureDate" ) values (${cid}, ${timestamp}) returning *`
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* advertise the vod segment(s) we captured.
|
||||
* futureporn/commander uses this data to elect one worker to upload the VOD
|
||||
*/
|
||||
async advertise () {
|
||||
const segments = await this.voddo.getRecordedSegments()
|
||||
const streams = Voddo.groupStreamSegments(segments)
|
||||
const workerId = this.workerId
|
||||
logger.log({ level: 'debug', message: `Advertising our VOD streams(s) ${JSON.stringify({segments, streams, workerId})}` })
|
||||
this.sql.notify('capture/vod/advertisement', JSON.stringify({segments, streams, workerId}))
|
||||
}
|
||||
|
||||
|
||||
listen () {
|
||||
this.sql.listen('scout/stream/stop', (data) => {
|
||||
logger.log({ level: 'debug', message: 'Scout said the stream has stopped. I will advertise the vod segment(s) I have.' })
|
||||
this.advertise()
|
||||
})
|
||||
|
||||
this.sql.listen('commander/vod/election', async (data) => {
|
||||
if (data.workerId === this.workerId) {
|
||||
logger.log({ level: 'debug', message: 'Commander elected me to process/upload' })
|
||||
this.process(await this.voddo.getFilenames())
|
||||
} else {
|
||||
logger.log({ level: 'debug', message: `Commander elected ${data.workerId} to process/upload their vod segment(s)` })
|
||||
}
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* process video(s) after end of stream
|
||||
*
|
||||
* @param {String[]} filenames
|
||||
* @returns {void}
|
||||
*/
|
||||
async process (filenames) {
|
||||
this.date = filenames[0].timestamp
|
||||
|
||||
logger.log({ level: 'debug', message: 'concatenation in progress...' })
|
||||
const file = await this.video.concat(filenames)
|
||||
|
||||
logger.log({ level: 'debug', message: `uploading ${file}` })
|
||||
const cid = await this.ipfs.upload(file)
|
||||
|
||||
logger.log({ level: 'debug', message: 'db save in progress' })
|
||||
await this.save(cid, this.date)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* download a livestream
|
||||
*
|
||||
* - initializes Voddo
|
||||
* - invokes this.process() as side effect
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
async download () {
|
||||
this.voddo.on('start', (data) => {
|
||||
logger.log({ level: 'debug', message: 'voddo started' })
|
||||
logger.log({ level: 'debug', message: data })
|
||||
this.sql.notify('capture/file', JSON.stringify(data))
|
||||
})
|
||||
this.voddo.on('stop', (report) => {
|
||||
logger.log({ level: 'debug', message: `Got a stop event from Voddo` })
|
||||
})
|
||||
logger.log({ level: 'debug', message: 'starting voddo' })
|
||||
this.voddo.start()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
|
||||
import {execa} from 'execa'
|
||||
import {loggerFactory} from 'common/logger'
|
||||
|
||||
const logger = loggerFactory({
|
||||
service: 'futureporn/capture'
|
||||
})
|
||||
|
||||
export default class Ipfs {
|
||||
constructor(opts) {
|
||||
this.multiaddr = opts?.IPFS_CLUSTER_HTTP_API_MULTIADDR
|
||||
this.username = opts?.IPFS_CLUSTER_HTTP_API_USERNAME
|
||||
this.password = opts?.IPFS_CLUSTER_HTTP_API_PASSWORD
|
||||
this.ctlExecutable = opts?.ctlExecutable || '/usr/local/bin/ipfs-cluster-ctl'
|
||||
this.ipfsExecutable = opts?.ipfsExecutable || '/usr/local/bin/ipfs'
|
||||
}
|
||||
getArgs () {
|
||||
let args = [
|
||||
'--no-check-certificate',
|
||||
'--host', this.multiaddr,
|
||||
'--basic-auth', `${this.username}:${this.password}`
|
||||
]
|
||||
return args
|
||||
}
|
||||
async upload (filename, expiryDuration = false) {
|
||||
try {
|
||||
let args = getArgs()
|
||||
|
||||
args = args.concat([
|
||||
'add',
|
||||
'--quieter',
|
||||
'--cid-version', 1
|
||||
])
|
||||
|
||||
if (expiryDuration) {
|
||||
args = args.concat(['--expire-in', expiryDuration])
|
||||
}
|
||||
|
||||
args.push(filename)
|
||||
|
||||
const { stdout } = await execa(this.ctlExecutable, args)
|
||||
return stdout
|
||||
} catch (e) {
|
||||
logger.log({ level: 'error', message: 'Error while adding file to ipfs' })
|
||||
logger.log({ level: 'error', message: e })
|
||||
}
|
||||
}
|
||||
async hash (filename) {
|
||||
try {
|
||||
const { stdout } = await execa(this.ipfsExecutable, ['add', '--quiet', '--cid-version=1', '--only-hash', filename])
|
||||
return stdout
|
||||
} catch (e) {
|
||||
logger.log({ level: 'error', message: 'Error while hashing file' })
|
||||
logger.log({ level: 'error', message: e })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
import { execa } from 'execa'
|
||||
import { tmpdir } from 'os'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
|
||||
export class VideoConcatError extends Error {
|
||||
constructor (msg) {
|
||||
super(msg || 'Failed to concatenate video')
|
||||
this.name = 'VideoConcatError'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default class Video {
|
||||
constructor (opts) {
|
||||
if (typeof opts.filePaths === 'undefined') throw new Error('Video must be called with opts.filePaths');
|
||||
if (typeof opts.cwd === 'undefined') throw new Error('Video must be called with opts.cwd');
|
||||
this.filePaths = opts.filePaths
|
||||
this.cwd = opts.cwd
|
||||
this.room = opts.room || 'projektmelody'
|
||||
this.execa = opts.execa || execa
|
||||
}
|
||||
|
||||
|
||||
|
||||
getFilesTxt () {
|
||||
return this.filePaths
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
.map((d) => `file '${d.file}'`)
|
||||
.join('\n')
|
||||
.concat('\n')
|
||||
}
|
||||
|
||||
|
||||
getFilesFile () {
|
||||
const p = path.join(this.cwd, 'files.txt')
|
||||
fs.writeFileSync(
|
||||
p,
|
||||
this.getFilesTxt(this.filePaths),
|
||||
{ encoding: 'utf-8' }
|
||||
)
|
||||
return p
|
||||
}
|
||||
|
||||
async concat () {
|
||||
const target = path.join(this.cwd, `${this.room}-chaturbate-${new Date().valueOf()}.mp4`)
|
||||
|
||||
const { exitCode, killed, stdout, stderr } = await this.execa('ffmpeg', [
|
||||
'-y',
|
||||
'-f', 'concat',
|
||||
'-safe', '0',
|
||||
'-i', this.getFilesFile(this.filePaths),
|
||||
'-c', 'copy',
|
||||
target
|
||||
], {
|
||||
cwd: this.cwd
|
||||
});
|
||||
|
||||
if (exitCode !== 0 || killed !== false) {
|
||||
throw new VideoConcatError(`exitCode:${exitCode}, killed:${killed}, stdout:${stdout}, stderr:${stderr}`);
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
import 'dotenv/config'
|
||||
import YoutubeDlWrap from "youtube-dl-wrap";
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { AbortController } from "node-abort-controller";
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
import { join } from 'node:path'
|
||||
import ffmpeg from 'fluent-ffmpeg'
|
||||
import { loggerFactory } from 'common/logger'
|
||||
|
||||
const logger = loggerFactory({
|
||||
service: 'futureporn/capture'
|
||||
})
|
||||
const defaultStats = {segments:[],lastUpdatedAt:null}
|
||||
|
||||
export default class Voddo extends EventEmitter {
|
||||
constructor(opts) {
|
||||
super()
|
||||
this.courtesyTimer = setTimeout(() => {}, 0);
|
||||
this.retryCount = 0;
|
||||
this.url = opts.url;
|
||||
this.format = opts.format || 'best';
|
||||
this.cwd = opts.cwd;
|
||||
this.ytdlee; // event emitter for ytdlwrap
|
||||
this.stats = Object.assign({}, defaultStats);
|
||||
this.abortController = new AbortController();
|
||||
this.ytdl = opts.ytdl || new YoutubeDlWrap();
|
||||
if (process.env.YOUTUBE_DL_BINARY) this.ytdl.setBinaryPath(process.env.YOUTUBE_DL_BINARY);
|
||||
}
|
||||
|
||||
static async getVideoLength (filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ffmpeg.ffprobe(filePath, function(err, metadata) {
|
||||
if (err) reject(err)
|
||||
resolve(Math.floor(metadata.format.duration*1000))
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// greets ChatGPT
|
||||
static groupStreamSegments(segments, threshold = 1000*60*60) {
|
||||
segments.sort((a, b) => a.startTime - b.startTime);
|
||||
const streams = [];
|
||||
let currentStream = [];
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const currentSegment = segments[i];
|
||||
const previousSegment = currentStream[currentStream.length - 1];
|
||||
|
||||
if (!previousSegment || currentSegment.startTime - previousSegment.endTime <= threshold) {
|
||||
currentStream.push(currentSegment);
|
||||
} else {
|
||||
streams.push(currentStream);
|
||||
currentStream = [currentSegment];
|
||||
}
|
||||
}
|
||||
|
||||
streams.push(currentStream);
|
||||
return streams;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* getRecordedStreams
|
||||
*
|
||||
* get the metadata of the videos captured
|
||||
*/
|
||||
async getRecordedSegments() {
|
||||
let f = []
|
||||
const files = await readdir(this.cwd).then((raw) => raw.filter((f) => /\.mp4$/.test(f) ))
|
||||
for (const file of files) {
|
||||
const filePath = join(this.cwd, file)
|
||||
const s = await stat(filePath)
|
||||
const videoDuration = await Voddo.getVideoLength(filePath)
|
||||
const startTime = parseInt(s.ctimeMs)
|
||||
const endTime = startTime+videoDuration
|
||||
const size = s.size
|
||||
f.push({
|
||||
startTime,
|
||||
endTime,
|
||||
file,
|
||||
size
|
||||
})
|
||||
}
|
||||
this.stats.segments = f
|
||||
|
||||
|
||||
return this.stats.segments
|
||||
}
|
||||
|
||||
isDownloading() {
|
||||
// if there are event emitter listeners for the progress event,
|
||||
// we are probably downloading.
|
||||
return (
|
||||
this.ytdlee?.listeners('progress').length !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
delayedStart() {
|
||||
// only for testing
|
||||
this.retryCount = 500
|
||||
this.courtesyTimer = this.getCourtesyTimer(() => this.download())
|
||||
}
|
||||
|
||||
|
||||
start() {
|
||||
// if download is in progress, do nothing
|
||||
if (this.isDownloading()) {
|
||||
logger.log({ level: 'debug', message: 'Doing nothing because a download is in progress.' })
|
||||
return;
|
||||
}
|
||||
|
||||
// if download is not in progress, start download immediately
|
||||
// reset the retryCount so the backoff timer resets to 1s between attempts
|
||||
this.retryCount = 0
|
||||
clearTimeout(this.courtesyTimer)
|
||||
|
||||
// create new abort controller
|
||||
//this.abortController = new AbortController() // @todo do i need this? Can't I reuse the existing this.abortController?
|
||||
|
||||
this.download()
|
||||
}
|
||||
|
||||
stop() {
|
||||
logger.log({ level: 'info', message: 'Received stop(). Stopping.' })
|
||||
clearTimeout(this.courtesyTimer)
|
||||
this.abortController.abort()
|
||||
}
|
||||
|
||||
/** generate a report **/
|
||||
getReport(errorMessage) {
|
||||
let report = {}
|
||||
report.stats = Object.assign({}, this.stats)
|
||||
report.error = errorMessage
|
||||
report.reason = (() => {
|
||||
if (errorMessage) return 'error';
|
||||
else if (this.abortController.signal.aborted) return 'aborted';
|
||||
else return 'close';
|
||||
})()
|
||||
// clear stats to prepare for next run
|
||||
this.stats = Object.assign({}, defaultStats)
|
||||
return report
|
||||
}
|
||||
|
||||
emitReport(report) {
|
||||
logger.log({ level: 'debug', message: 'EMITTING REPORT' })
|
||||
this.emit('stop', report)
|
||||
}
|
||||
|
||||
getCourtesyTimer(callback) {
|
||||
// 600000ms = 10m
|
||||
const waitTime = Math.min(600000, (Math.pow(2, this.retryCount) * 1000));
|
||||
this.retryCount += 1;
|
||||
logger.log({ level: 'debug', message: `courtesyWait for ${waitTime/1000} seconds. (retryCount: ${this.retryCount})` })
|
||||
return setTimeout(callback, waitTime)
|
||||
}
|
||||
|
||||
download() {
|
||||
const handleProgress = (progress) => {
|
||||
logger.log({ level: 'debug', message:` [*] progress event` })
|
||||
this.stats.lastUpdatedAt = Date.now(),
|
||||
this.stats.totalSize = progress.totalSize
|
||||
}
|
||||
|
||||
const handleError = (error) => {
|
||||
if (error?.message !== undefined && error.message.includes('Room is currently offline')) {
|
||||
logger.log({ level: 'debug', message: 'Handled an expected \'Room is offline\' error' })
|
||||
|
||||
} else {
|
||||
logger.log({ level: 'error', message: 'ytdl error' })
|
||||
logger.log({ level: 'error', message: error.message })
|
||||
}
|
||||
this.ytdlee.off('progress', handleProgress)
|
||||
this.ytdlee.off('handleYtdlEvent', handleYtdlEvent)
|
||||
|
||||
// restart the download after the courtesyTimeout
|
||||
this.courtesyTimer = this.getCourtesyTimer(() => this.download())
|
||||
this.emitReport(this.getReport(error.message))
|
||||
}
|
||||
|
||||
|
||||
const handleYtdlEvent = (type, data) => {
|
||||
logger.log({ level: 'debug', message: `handleYtdlEvent type: ${type}, data: ${data}` })
|
||||
logger.log({ level: 'debug', message: `handleYtdlEvent type: ${type}, data: ${data}` })
|
||||
if (type === 'download' && data.includes('Destination:')) {
|
||||
let filePath = /Destination:\s(.*)$/.exec(data)[1]
|
||||
logger.log({ level: 'debug', message: `Destination file detected: ${filePath}` })
|
||||
let datum = { file: filePath, timestamp: new Date().valueOf() }
|
||||
let segments = this.stats.segments
|
||||
segments.push(datum) && segments.length > 64 && segments.shift(); // limit the size of the segments array
|
||||
this.emit('start', datum)
|
||||
} else if (type === 'ffmpeg' && data.includes('bytes')) {
|
||||
const bytes = /(\d*)\sbytes/.exec(data)[1]
|
||||
logger.log({ level: 'debug', message: `ffmpeg reports ${bytes}`})
|
||||
let mostRecentFile = this.stats.segments[this.stats.segments.length-1]
|
||||
mostRecentFile['size'] = bytes
|
||||
logger.log({ level: 'debug', message: mostRecentFile })
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
logger.log({ level: 'debug', message: 'got a close event. handling!' });
|
||||
|
||||
this.ytdlee.off('progress', handleProgress)
|
||||
this.ytdlee.off('handleYtdlEvent', handleYtdlEvent)
|
||||
|
||||
// restart Voddo only if the close was not due to stop()
|
||||
if (!this.abortController.signal.aborted) {
|
||||
// restart the download after the courtesyTimeout
|
||||
this.courtesyTimer = this.getCourtesyTimer(() => this.download())
|
||||
}
|
||||
|
||||
this.emitReport(this.getReport())
|
||||
}
|
||||
|
||||
logger.log({ level: 'debug', message: `Downloading url:${this.url} format:${this.format}` })
|
||||
logger.log({ level: 'debug', message: JSON.stringify(this.ytdl) })
|
||||
|
||||
// sanity check. ensure cwd exists
|
||||
stat(this.cwd, (err) => {
|
||||
if (err) logger.log({ level: 'error', message: `Error while getting cwd stats of ${this.cwd} Does it exist?` })
|
||||
})
|
||||
|
||||
this.ytdlee = this.ytdl.exec(
|
||||
[this.url, '-f', this.format],
|
||||
{
|
||||
cwd: this.cwd
|
||||
},
|
||||
this.abortController.signal
|
||||
);
|
||||
this.ytdlee.on('progress', handleProgress);
|
||||
this.ytdlee.on('youtubeDlEvent', handleYtdlEvent);
|
||||
this.ytdlee.once('error', handleError);
|
||||
this.ytdlee.once('close', handleClose);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import cheerio from 'cheerio'
|
||||
import fetch from 'node-fetch'
|
||||
|
||||
export async function getRandomRoom () {
|
||||
const res = await fetch('https://chaturbate.com/')
|
||||
const body = await res.text()
|
||||
const $ = cheerio.load(body)
|
||||
let roomsRaw = $('a[data-room]')
|
||||
let rooms = []
|
||||
$(roomsRaw).each((_, e) => {
|
||||
rooms.push($(e).attr('href'))
|
||||
})
|
||||
|
||||
// greets https://stackoverflow.com/a/4435017/1004931
|
||||
var randomIndex = Math.floor(Math.random() * rooms.length);
|
||||
return rooms[randomIndex].replaceAll('/', '')
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import disk from 'diskusage';
|
||||
|
||||
|
||||
export function verifyStorage (appContext) {
|
||||
const mountPath = appContext.env.FUTUREPORN_WORKDIR
|
||||
disk.check(mountPath, (err, info) => {
|
||||
if (err) {
|
||||
appContext.logger.log({ level: 'error', message: `Error retrieving disk usage for ${mountPath}: ${err}` });
|
||||
return;
|
||||
}
|
||||
|
||||
const totalSize = info.total;
|
||||
const availableSize = info.available;
|
||||
const freeSize = info.free;
|
||||
|
||||
appContext.logger.log({ 'level': 'info', message: `${mountPath} Disk Usage:` });
|
||||
appContext.logger.log({ 'level': 'info', message: `Total: ${bytesToSize(totalSize)}` });
|
||||
appContext.logger.log({ 'level': 'info', message: `Free: ${bytesToSize(freeSize)}` });
|
||||
appContext.logger.log({ 'level': 'info', message: `Available: ${bytesToSize(availableSize)}` });
|
||||
|
||||
if (availableSize < 85899345920) appContext.logger.log({ 'level': 'warn', message: `⚠️ Available disk is getting low! ${bytesToSize(availableSize)}` });
|
||||
else if (availableSize < 42949672960) appContext.logger.log({ 'level': 'error', message: `⚠️☠️ AVAILABLE DISK IS TOO LOW! ${bytesToSize(availableSize)}` });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Helper function to convert bytes to human-readable format
|
||||
export function bytesToSize(bytes) {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const i = Math.floor(Math.log2(bytes) / 10);
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import winston from 'winston'
|
||||
|
||||
export const loggerFactory = (options) => {
|
||||
const mergedOptions = Object.assign({}, {
|
||||
level: 'info',
|
||||
defaultMeta: { service: 'futureporn' },
|
||||
format: winston.format.timestamp()
|
||||
}, options)
|
||||
const logger = winston.createLogger(mergedOptions);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
level: 'debug',
|
||||
format: winston.format.simple()
|
||||
}))
|
||||
} else {
|
||||
logger.add(new winston.transports.Console({
|
||||
level: 'info',
|
||||
format: winston.format.json()
|
||||
}))
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import { join } from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import fs from 'node:fs';
|
||||
|
||||
export const getFilename = (appContext, roomName) => {
|
||||
const name = `${roomName}_${new Date().toISOString()}.ts`
|
||||
return join(appContext.env.FUTUREPORN_WORKDIR, 'recordings', name);
|
||||
}
|
||||
|
||||
|
||||
export const assertDirectory = (directoryPath) => {
|
||||
if (fs.statSync(directoryPath, { throwIfNoEntry: false }) === undefined) fs.mkdirSync(directoryPath);
|
||||
}
|
||||
|
||||
export const checkFFmpeg = async (appContext) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const childProcess = spawn('ffmpeg', ['-version']);
|
||||
|
||||
childProcess.on('error', (err) => {
|
||||
appContext.logger.log({
|
||||
level: 'error',
|
||||
message: `ffmpeg -version failed, which likely means ffmpeg is not installed or not on $PATH`,
|
||||
});
|
||||
throw new Error('ffmpeg is missing')
|
||||
});
|
||||
|
||||
childProcess.on('exit', (code) => {
|
||||
if (code !== 0) reject(`'ffmpeg -version' exited with code ${code}`)
|
||||
if (code === 0) {
|
||||
appContext.logger.log({ level: 'info', message: `ffmpeg PRESENT.` });
|
||||
resolve()
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
export const assertDependencyDirectory = (appContext) => {
|
||||
// Extract the directory path from the filename
|
||||
const directoryPath = join(appContext.env.FUTUREPORN_WORKDIR, 'recordings');
|
||||
console.log(`asserting ${directoryPath} exists`)
|
||||
|
||||
// Check if the directory exists, and create it if it doesn't
|
||||
if (!fs.existsSync(directoryPath)) {
|
||||
fs.mkdirSync(directoryPath, { recursive: true });
|
||||
console.log(`Created directory: ${directoryPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const record = async (appContext, playlistUrl, roomName) => {
|
||||
if (appContext === undefined) throw new Error('appContext undef');
|
||||
if (playlistUrl === undefined) throw new Error('playlistUrl undef');
|
||||
if (roomName === undefined) throw new Error('roomName undef');
|
||||
|
||||
const filename = getFilename(appContext, roomName);
|
||||
console.log(`downloading to ${filename}`)
|
||||
|
||||
// example: `ffmpeg -headers "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0"
|
||||
// -i ${chunkPlaylist}
|
||||
// -c:v copy
|
||||
// -c:a copy
|
||||
// -movflags faststart
|
||||
// -y
|
||||
// -f mpegts
|
||||
// ./my-recording.ts`
|
||||
const ffmpegProcess = spawn('ffmpeg', [
|
||||
'-headers', `"User-Agent: ${appContext.env.DOWNLOADER_UA}"`,
|
||||
'-i', playlistUrl,
|
||||
'-c:v', 'copy',
|
||||
'-c:a', 'copy',
|
||||
'-movflags', 'faststart',
|
||||
'-y',
|
||||
'-f', 'mpegts',
|
||||
filename
|
||||
], {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ffmpegProcess.once('exit', (code) => {
|
||||
resolve(code)
|
||||
})
|
||||
})
|
||||
|
||||
// ffmpegProcess.on('data', (data) => {
|
||||
// console.log(data.toString());
|
||||
// });
|
||||
|
||||
|
||||
// Optional: Handle other events such as 'error', 'close', etc.
|
||||
// @todo this needs to be handled outside this function
|
||||
// otherwise this function is not testable
|
||||
// ffmpegProcess.on('exit', (code, signal) => {
|
||||
// // Retry the download using exponential backoff if the process exits for any reason
|
||||
// console.log(`ffmpeg exited with code ${code} and signal ${signal}`)
|
||||
// retryDownload(appContext, playlistUrl, roomName);
|
||||
// });
|
||||
|
||||
// return ffmpegProcess;
|
||||
}
|
||||
|
||||
|
||||
const calculateExponentialBackoffDelay = (attemptNumber) => {
|
||||
return Math.pow(2, attemptNumber) * 1000;
|
||||
};
|
||||
|
||||
const retryDownload = (appContext, playlistUrl, roomName, attemptNumber = 1, maxAttempts = 3) => {
|
||||
const delay = calculateExponentialBackoffDelay(attemptNumber);
|
||||
|
||||
appContext.logger.log({ level: 'debug', message: `Retrying download in ${delay / 1000} seconds...` });
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('Retrying download...');
|
||||
record(appContext, playlistUrl, roomName, attemptNumber + 1);
|
||||
}, delay);
|
||||
};
|
|
@ -0,0 +1,147 @@
|
|||
|
||||
import Video from '../src/Video.js'
|
||||
import Capture from '../src/Capture.js'
|
||||
import Ipfs from '../src/Ipfs.js'
|
||||
import chai, { expect } from 'chai'
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'node:path'
|
||||
import sinon from 'sinon'
|
||||
import sinonChai from 'sinon-chai'
|
||||
import { CID } from 'multiformats/cid'
|
||||
import Voddo from '../src/Voddo.js'
|
||||
import EventEmitter from 'node:events'
|
||||
import postgres from 'postgres'
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
const Timer = setTimeout(()=>{},0).constructor
|
||||
const fixtureDate = 1581117660000
|
||||
const cidFixture = 'bafybeid3mg5lzrvnmpfi5ftwhiupp7i5bgkmdo7dnlwrvklbv33telrrry'
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe('Capture', function () {
|
||||
|
||||
let clock
|
||||
|
||||
const sandbox = sinon.createSandbox()
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
clock = sandbox.useFakeTimers({
|
||||
toFake: ["setTimeout", "setInterval"],
|
||||
shouldAdvanceTime: false
|
||||
});
|
||||
|
||||
// // const sql = postgres({
|
||||
// // idle_timeout: 1
|
||||
// // })
|
||||
|
||||
// let pgStub = (opts) => {
|
||||
// let sql = (args) => {}
|
||||
// return sql
|
||||
// }
|
||||
const sqlRaw = postgres()
|
||||
const sql = sandbox.stub(sqlRaw)
|
||||
// sql.listen.resolves(fixtureDate)
|
||||
// sql.notify.resolves(92834)
|
||||
// sinon.stub(postgres, 'notify')
|
||||
// sinon.createStubInstance(postgres)
|
||||
// sql
|
||||
// .withArgs('INSERT INTO vod ( videoSrcHash, captureDate ) values (bafybeid3mg5lzrvnmpfi5ftwhiupp7i5bgkmdo7dnlwrvklbv33telrrry, 1581117660000) returning *')
|
||||
// .resolves({ msg: 'idk' })
|
||||
// sinon.stub(sql, 'notify').returns()
|
||||
|
||||
|
||||
|
||||
|
||||
// const ipfs = sandbox.createStubInstance(Ipfs)
|
||||
// ipfs.upload.withArgs('/tmp/mycoolfile.mp4').resolves(cidFixture)
|
||||
// capture = new Capture({
|
||||
// sql,
|
||||
// ipfs,
|
||||
// video,
|
||||
// voddo
|
||||
// })
|
||||
// sandbox.stub(capture, 'process').resolves()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
|
||||
|
||||
describe('upload', function () {
|
||||
it('should upload a video to ipfs', async function () {
|
||||
|
||||
const sqlRaw = postgres()
|
||||
const sql = sandbox.stub(sqlRaw)
|
||||
|
||||
const video = sandbox.stub()
|
||||
const voddo = sandbox.createStubInstance(Voddo)
|
||||
voddo.on.callThrough()
|
||||
voddo.emit.callThrough()
|
||||
voddo.listeners.callThrough()
|
||||
voddo.listenerCount.callThrough()
|
||||
|
||||
|
||||
voddo.start.callsFake(() => {
|
||||
voddo.emit('start', { file: '/tmp/burrito.mp4', timestamp: 1 })
|
||||
})
|
||||
|
||||
const ipfs = sandbox.createStubInstance(Ipfs)
|
||||
ipfs.upload.withArgs('/tmp/mycoolfile.mp4').resolves(cidFixture)
|
||||
const capture = new Capture({
|
||||
sql,
|
||||
ipfs,
|
||||
video,
|
||||
voddo
|
||||
})
|
||||
|
||||
const cid = await capture.upload('/tmp/mycoolfile.mp4')
|
||||
expect(() => CID.parse(cid), `The IPFS CID '${cid}' is invalid.`).to.not.throw()
|
||||
expect(capture.ipfs.upload).calledOnce
|
||||
})
|
||||
})
|
||||
describe('save', function () {
|
||||
it('should save to db', async function () {
|
||||
|
||||
const sqlRaw = postgres()
|
||||
const sql = sandbox.stub(sqlRaw)
|
||||
|
||||
const video = sandbox.stub()
|
||||
const voddo = sandbox.createStubInstance(Voddo)
|
||||
voddo.on.callThrough()
|
||||
voddo.emit.callThrough()
|
||||
voddo.listeners.callThrough()
|
||||
voddo.listenerCount.callThrough()
|
||||
|
||||
|
||||
voddo.start.callsFake(() => {
|
||||
voddo.emit('start', { file: '/tmp/burrito.mp4', timestamp: 1 })
|
||||
})
|
||||
|
||||
const ipfs = sandbox.createStubInstance(Ipfs)
|
||||
ipfs.upload.withArgs('/tmp/mycoolfile.mp4').resolves(cidFixture)
|
||||
const capture = new Capture({
|
||||
sql,
|
||||
ipfs,
|
||||
video,
|
||||
voddo
|
||||
})
|
||||
|
||||
// I can't stub sql`` because of that template string override so i'm just stubbing capture.save
|
||||
// I think this is an evergreen test ¯\_(ツ)_/¯
|
||||
sandbox.stub(capture, 'save').resolves([
|
||||
{ id: 1, cid: cidFixture, captureDate: fixtureDate }
|
||||
])
|
||||
const vod = await capture.save(cidFixture, fixtureDate)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
})
|
|
@ -0,0 +1,86 @@
|
|||
|
||||
import 'dotenv/config'
|
||||
import Video from '../src/Video.js'
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'node:path'
|
||||
import os from 'node:os'
|
||||
import fs from 'node:fs'
|
||||
import sinon from 'sinon'
|
||||
import sinonChai from 'sinon-chai'
|
||||
import chai, { expect } from 'chai'
|
||||
|
||||
chai.use(sinonChai);
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
|
||||
|
||||
const dataFixture = [
|
||||
{
|
||||
timestamp: 1,
|
||||
file: 'mock-stream0.mp4'
|
||||
}, {
|
||||
timestamp: 2,
|
||||
file: 'mock-stream1.mp4'
|
||||
}, {
|
||||
timestamp: 3,
|
||||
file: 'mock-stream2.mp4'
|
||||
}
|
||||
]
|
||||
|
||||
describe('Video', function () {
|
||||
|
||||
let video
|
||||
|
||||
before(() => {
|
||||
// copy files to /tmp so we dont clutter the fixtures dir
|
||||
// and simulate cwd being process.env.FUTUREPORN_TMP
|
||||
dataFixture.forEach((d) => {
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, 'fixtures', d.file),
|
||||
path.join(os.tmpdir(), d.file)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
video = new Video({
|
||||
cwd: os.tmpdir(),
|
||||
filePaths: dataFixture,
|
||||
execa: sinon.fake.resolves({ exitCode: 0, killed: false, stdout: "i am so horni rn", stderr: null })
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
console.log('>> sinon.restore! (afterEach)')
|
||||
sinon.restore();
|
||||
})
|
||||
|
||||
|
||||
describe('getFilesTxt', function () {
|
||||
it('should generate contents suitable for input to `ffmpeg -f concat`', function () {
|
||||
const txt = video.getFilesTxt()
|
||||
expect(txt).to.deep.equal("file 'mock-stream0.mp4'\nfile 'mock-stream1.mp4'\nfile 'mock-stream2.mp4'\n")
|
||||
})
|
||||
})
|
||||
|
||||
describe('concat', function () {
|
||||
it('should join multiple videos into one', async function () {
|
||||
const file = await video.concat()
|
||||
expect(typeof file === 'string').to.be.true
|
||||
expect(video.execa).calledOnce
|
||||
expect(file).to.match(/\.mp4$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getFilesFile', function () {
|
||||
it('should create a files.txt and return the path', async function () {
|
||||
const file = await video.getFilesFile()
|
||||
expect(typeof file === 'string').to.be.true
|
||||
expect(file).to.equal(path.join(os.tmpdir(), 'files.txt'))
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,491 @@
|
|||
import 'dotenv/config'
|
||||
import Voddo from '../src/Voddo.js'
|
||||
import chai, { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import YoutubeDlWrap from 'youtube-dl-wrap'
|
||||
import {
|
||||
AbortController
|
||||
} from "node-abort-controller";
|
||||
import {
|
||||
EventEmitter
|
||||
} from 'events'
|
||||
import debugFactory from 'debug'
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import sinonChai from 'sinon-chai'
|
||||
import sinonTest from "sinon-test";
|
||||
import path from 'path'
|
||||
|
||||
chai.use(sinonChai);
|
||||
|
||||
const test = sinonTest(sinon, {
|
||||
toFake: ["setTimeout", "setInterval"],
|
||||
shouldAdvanceTime: false
|
||||
});
|
||||
const debug = debugFactory('voddo')
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
describe('Voddo', function() {
|
||||
|
||||
|
||||
describe('groupStreamSegments', function () {
|
||||
it('should separate two stream data objects', function () {
|
||||
const fixture = [{
|
||||
"startTime": 1675386000000,
|
||||
"file": "projektmelody 2023-02-02 17_00-projektmelody.mp4",
|
||||
"size": 550799038,
|
||||
"endTime": 1675391400000,
|
||||
}, {
|
||||
"startTime": 1675391405000,
|
||||
"file": "projektmelody 2023-02-02 18_30-projektmelody.mp4",
|
||||
"size": 6556534941,
|
||||
"endTime": 1675396800000
|
||||
}, {
|
||||
"startTime": 1675368000000,
|
||||
"file": "projektmelody 2023-02-02 12_00-projektmelody.mp4",
|
||||
"size": 6556534941,
|
||||
"endTime": 1675378800000
|
||||
}]
|
||||
|
||||
const streams = Voddo.groupStreamSegments(fixture)
|
||||
expect(streams).to.deep.equal([
|
||||
[
|
||||
{
|
||||
"startTime": 1675368000000,
|
||||
"file": "projektmelody 2023-02-02 12_00-projektmelody.mp4",
|
||||
"size": 6556534941,
|
||||
"endTime": 1675378800000
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"startTime": 1675386000000,
|
||||
"file": "projektmelody 2023-02-02 17_00-projektmelody.mp4",
|
||||
"size": 550799038,
|
||||
"endTime": 1675391400000,
|
||||
}, {
|
||||
"startTime": 1675391405000,
|
||||
"file": "projektmelody 2023-02-02 18_30-projektmelody.mp4",
|
||||
"size": 6556534941,
|
||||
"endTime": 1675396800000
|
||||
}
|
||||
]
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// let clock;
|
||||
|
||||
// beforeEach(function() {
|
||||
// clock = sinon.useFakeTimers({
|
||||
// toFake: ["setTimeout", "setInterval"],
|
||||
// shouldAdvanceTime: false
|
||||
// });
|
||||
// })
|
||||
|
||||
// afterEach(() => {
|
||||
// sinon.restore()
|
||||
// })
|
||||
|
||||
|
||||
|
||||
// Something faulty with Voddo or sinon or mocha, not sure.
|
||||
// When running by itself, test succeeds. When running with 'should start and stop stream download',
|
||||
// voddo.stats gets set to whatever that test sets it to. So bizarre, it's like the same Voddo class instance
|
||||
// exists in two different tests even though they are named differently.
|
||||
// Even though they are not in global scope. Even though each was called with `new Voddo(...)`
|
||||
// Doesn't matter if I wrap both in sinon-test. Same leaky problem.
|
||||
// Doesn't matter if I sinon.restore() afterEach. Same leaky problem.
|
||||
// Doesn't matter if I manually set up a sinon sandbox. Same leaky problem.
|
||||
// Fuck event emitters. I love their utility but I don't know how the fuck they are supposed to be tested.
|
||||
// Solution might just call for a rewrite of Voddo, or perhaps deleting Voddo in favor of Capture
|
||||
// For now, I'm moving forward because Voddo works even though this test does not.
|
||||
describe('getRecordedSegments', function() {
|
||||
xit('should populate it\'s log if log is empty', async function () {
|
||||
const voddo = new Voddo({
|
||||
url: 'https://example.com',
|
||||
cwd: join(__dirname, 'fixtures')
|
||||
})
|
||||
const streams = await voddo.getRecordedSegments()
|
||||
console.log(streams)
|
||||
expect(streams.length).to.equal(3)
|
||||
expect(streams[0]).to.have.property('startTime')
|
||||
expect(streams[0]).to.have.property('file')
|
||||
expect(streams[0]).to.have.property('size')
|
||||
})
|
||||
xit('should use Voddo\'s stats history to get filenames of only the most recent stream', async function() {
|
||||
const sb = sinon.createSandbox()
|
||||
const viddo = new Voddo({
|
||||
url: 'https://example.com',
|
||||
cwd: '~/Downloads'
|
||||
})
|
||||
sb.stub(viddo, 'stats').value({
|
||||
segments: [{
|
||||
startTime: 1674147647000,
|
||||
size: 192627,
|
||||
file: 'projektmelody 2023-01-19 17_00-projektmelody.mp4'
|
||||
}, {
|
||||
startTime: 1674151247000,
|
||||
size: 192627,
|
||||
file: 'projektmelody 2023-01-19 18_00-projektmelody.mp4'
|
||||
}, {
|
||||
startTime: 1674154847000,
|
||||
size: 192627,
|
||||
file: 'projektmelody 2023-01-19 19_00-projektmelody.mp4'
|
||||
}, {
|
||||
file: 'projektmelody 2023-01-20 20_10-projektmelody.mp4',
|
||||
size: 192627,
|
||||
startTime: 1674245400000,
|
||||
}, {
|
||||
file: 'projektmelody 2023-01-20 21_10-projektmelody.mp4',
|
||||
size: 192627,
|
||||
startTime: 1674249000000,
|
||||
}, {
|
||||
file: 'projektmelody 2023-01-20 22_10-projektmelody.mp4',
|
||||
size: 192627,
|
||||
startTime: 1674252600000,
|
||||
}]
|
||||
})
|
||||
|
||||
const filenames = await viddo.getRecordedSegments()
|
||||
sb.restore()
|
||||
expect(filenames).to.have.lengthOf(3)
|
||||
expect(filenames).to.deep.equal([{
|
||||
file: 'projektmelody 2023-01-20 20_10-projektmelody.mp4',
|
||||
size: 192627,
|
||||
startTime: 1674245400000,
|
||||
}, {
|
||||
file: 'projektmelody 2023-01-20 21_10-projektmelody.mp4',
|
||||
size: 192627,
|
||||
startTime: 1674249000000,
|
||||
}, {
|
||||
file: 'projektmelody 2023-01-20 22_10-projektmelody.mp4',
|
||||
size: 192627,
|
||||
startTime: 1674252600000,
|
||||
}])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
xit('should keep a log of the files downloaded', function(done) {
|
||||
const ee = new EventEmitter()
|
||||
|
||||
|
||||
const ytdl = sinon.createStubInstance(YoutubeDlWrap)
|
||||
ytdl.exec.returns(ee)
|
||||
|
||||
|
||||
const times = [
|
||||
1000, // start
|
||||
1000 * 60 * 60 * 1, // stop
|
||||
1000 * 60 * 60 * 1 + 1, // start
|
||||
1000 * 60 * 60 * 2, // stop
|
||||
1000 * 60 * 60 * 3 + 1, // start
|
||||
1000 * 60 * 60 * 4 // stop
|
||||
]
|
||||
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('youtubeDlEvent', 'download', ' Destination: projektmelody 2023-01-18 21_10-projektmelody.mp4')
|
||||
}, times[0])
|
||||
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('close')
|
||||
}, times[1])
|
||||
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('youtubeDlEvent', 'download', ' Destination: projektmelody 2023-01-18 22_10-projektmelody.mp4')
|
||||
}, times[2])
|
||||
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('close')
|
||||
}, times[3])
|
||||
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('youtubeDlEvent', 'download', ' Destination: projektmelody 2023-01-18 23_10-projektmelody.mp4')
|
||||
}, times[4])
|
||||
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('close')
|
||||
}, times[5])
|
||||
|
||||
|
||||
let url = `https://chaturbate.com/projektmelody`
|
||||
let cwd = process.env.FUTUREPORN_WORKDIR || '/tmp'
|
||||
const voddo = new Voddo({
|
||||
url: url,
|
||||
format: 'best',
|
||||
cwd: cwd,
|
||||
ytdl
|
||||
})
|
||||
|
||||
voddo.once('start', (data) => {
|
||||
expect(data).to.have.property('file')
|
||||
expect(data).to.have.property('timestamp')
|
||||
|
||||
voddo.once('start', (data) => {
|
||||
expect(data).to.have.property('file')
|
||||
expect(data).to.have.property('timestamp')
|
||||
|
||||
voddo.once('start', (data) => {
|
||||
expect(data).to.have.property('file')
|
||||
expect(data).to.have.property('timestamp')
|
||||
|
||||
voddo.once('stop', function(report) {
|
||||
debug(report)
|
||||
expect(report).to.have.property('stats')
|
||||
expect(report.stats).to.have.property('files')
|
||||
expect(report.stats.files).to.have.lengthOf(3)
|
||||
debug(report.stats.files)
|
||||
expect(report.stats.files[0]).to.include({
|
||||
file: 'projektmelody 2023-01-18 21_10-projektmelody.mp4'
|
||||
})
|
||||
|
||||
expect(ytdl.exec).calledThrice
|
||||
|
||||
console.log('>>WE ARE DONE')
|
||||
expect(this.clock.countTimers()).to.equal(0)
|
||||
done()
|
||||
})
|
||||
clock.tick(times[5]) // stop
|
||||
|
||||
})
|
||||
clock.tick(times[3]) // stop
|
||||
clock.tick(times[4]) // start
|
||||
|
||||
})
|
||||
clock.tick(times[1]) // stop
|
||||
clock.tick(times[2]) // start
|
||||
|
||||
})
|
||||
|
||||
|
||||
voddo.start()
|
||||
expect(ytdl.exec).calledOnce
|
||||
|
||||
clock.tick(times[0])
|
||||
|
||||
|
||||
|
||||
|
||||
})
|
||||
|
||||
xit('should keep a log of the files downloaded', function(done) {
|
||||
this.timeout(5000)
|
||||
// https://github.com/insanity54/futureporn/issues/13
|
||||
const ytdlStub = sinon.createStubInstance(YoutubeDlWrap)
|
||||
ytdlStub.exec
|
||||
.onCall(0)
|
||||
.callsFake(function(args, opts, aborter) {
|
||||
let ee = new EventEmitter()
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('youtubeDlEvent', 'download', ' Destination: projektmelody 2023-01-18 21_10-projektmelody.mp4')
|
||||
}, 50)
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('close')
|
||||
}, 100)
|
||||
return ee
|
||||
})
|
||||
.onCall(1)
|
||||
.callsFake(function(args, opts, aborter) {
|
||||
let ee = new EventEmitter()
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('youtubeDlEvent', 'download', ' Destination: projektmelody 2023-01-18 22_10-projektmelody.mp4')
|
||||
}, 50)
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('close')
|
||||
}, 100)
|
||||
return ee
|
||||
})
|
||||
.onCall(2)
|
||||
.callsFake(function(args, opts, aborter) {
|
||||
let ee = new EventEmitter()
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('youtubeDlEvent', 'download', ' Destination: projektmelody 2023-01-18 23_10-projektmelody.mp4')
|
||||
}, 50)
|
||||
clock.setTimeout(() => {
|
||||
ee.emit('close')
|
||||
}, 100)
|
||||
return ee
|
||||
})
|
||||
let url = `https://chaturbate.com/projektmelody`
|
||||
let cwd = process.env.FUTUREPORN_WORKDIR || '/tmp'
|
||||
|
||||
const voddo = new Voddo({
|
||||
url: url,
|
||||
format: 'best',
|
||||
cwd: cwd,
|
||||
ytdl: ytdlStub
|
||||
})
|
||||
|
||||
// expect(clock.countTimers()).to.equal(0)
|
||||
voddo.once('start', function(data) {
|
||||
expect(data).to.have.property('file')
|
||||
expect(data).to.have.property('timestamp')
|
||||
|
||||
clock.next()
|
||||
clock.next()
|
||||
voddo.once('start', function(data) {
|
||||
expect(data).to.have.property('file')
|
||||
expect(data).to.have.property('timestamp')
|
||||
|
||||
voddo.once('start', function(data) {
|
||||
debug('fake start?')
|
||||
expect(data).to.have.property('file')
|
||||
expect(data).to.have.property('timestamp')
|
||||
|
||||
voddo.once('stop', function(report) {
|
||||
debug(report)
|
||||
expect(report).to.have.property('stats')
|
||||
expect(report.stats).to.have.property('files')
|
||||
expect(report.stats.files).to.have.lengthOf(3)
|
||||
debug(report.stats.files)
|
||||
expect(report.stats.files[0]).to.include({
|
||||
file: 'projektmelody 2023-01-18 21_10-projektmelody.mp4'
|
||||
})
|
||||
|
||||
sinon.assert.calledThrice(ytdlStub.exec)
|
||||
expect(this.clock.countTimers()).to.equal(0)
|
||||
done()
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
voddo.start()
|
||||
})
|
||||
|
||||
|
||||
it('should start and stop stream download', test(function(done) {
|
||||
|
||||
const sandbox = this
|
||||
|
||||
const ee = new EventEmitter()
|
||||
|
||||
const ytdl = this.createStubInstance(YoutubeDlWrap);
|
||||
ytdl.exec.returns(ee)
|
||||
|
||||
|
||||
const url = 'https://chaturbate.com/projektmelody'
|
||||
const format = 'best'
|
||||
const cwd = '/tmp'
|
||||
const v = new Voddo({
|
||||
url,
|
||||
format,
|
||||
cwd,
|
||||
ytdl
|
||||
})
|
||||
console.log(v.stats)
|
||||
|
||||
v.once('stop', function(data) {
|
||||
console.log('ffffff')
|
||||
console.log(this)
|
||||
expect(this.abortController.signal.aborted, 'abortController did not abort').to.be.true
|
||||
expect(sandbox.clock.countTimers()).to.equal(0)
|
||||
done()
|
||||
})
|
||||
v.once('start', function(data) {
|
||||
console.log('STARRRRRT')
|
||||
expect(data).to.have.property('file')
|
||||
expect(data).to.have.property('timestamp')
|
||||
expect(this).to.have.property('abortController')
|
||||
console.log('ey cool, voddo started')
|
||||
})
|
||||
v.start()
|
||||
|
||||
const times = [
|
||||
500,
|
||||
1000,
|
||||
2000
|
||||
]
|
||||
|
||||
this.clock.setTimeout(() => {
|
||||
ee.emit('youtubeDlEvent', 'download', ' Destination: projektmelody 2023-01-18 21_10-projektmelody.mp4')
|
||||
}, times[0])
|
||||
|
||||
this.clock.setTimeout(() => {
|
||||
v.stop()
|
||||
}, times[1])
|
||||
|
||||
this.clock.setTimeout(() => {
|
||||
ee.emit('close')
|
||||
}, times[2])
|
||||
|
||||
this.clock.tick(times[0]) // start
|
||||
this.clock.tick(times[1]) // stop
|
||||
this.clock.tick(times[2]) // close
|
||||
|
||||
}))
|
||||
|
||||
|
||||
xit('should retry when a stream closes', function(done) {
|
||||
|
||||
const ytdlStub = sinon.createStubInstance(YoutubeDlWrap);
|
||||
ytdlStub.exec
|
||||
.onCall(0)
|
||||
.callsFake(function(args, opts, aborter) {
|
||||
debug(' [test] callsFake 0')
|
||||
let ee = new EventEmitter()
|
||||
setTimeout(() => {
|
||||
console.log('should retry when a stream closes -- emission')
|
||||
ee.emit('youtubeDlEvent', 'download', ' Destination: projektmelody 2023-01-17 19_39-projektmelody.mp4')
|
||||
}, 100)
|
||||
setTimeout(() => {
|
||||
console.log('should retry when a stream closes -- emission')
|
||||
// this simulates youtube-dl closing
|
||||
// (NOT Voddo closing)
|
||||
ee.emit('close')
|
||||
}, 550)
|
||||
return ee
|
||||
})
|
||||
.onCall(1)
|
||||
.callsFake(function(args, opts, aborter) {
|
||||
debug(' [test] callsFake 1')
|
||||
let ee = new EventEmitter()
|
||||
setTimeout(() => {
|
||||
ee.emit('youtubeDlEvent', 'download', ' Destination: projektmelody 2023-01-17 19_45-projektmelody.mp4')
|
||||
}, 100)
|
||||
return ee
|
||||
})
|
||||
let url = `https://chaturbate.com/projektmelody`
|
||||
let cwd = process.env.FUTUREPORN_WORKDIR || '/tmp'
|
||||
let abortController = new AbortController()
|
||||
|
||||
const voddo = new Voddo({
|
||||
url: url,
|
||||
format: 'best',
|
||||
cwd: cwd,
|
||||
ytdl: ytdlStub
|
||||
})
|
||||
|
||||
voddo.once('start', function(data) {
|
||||
debug(' [test] voddo <<<<<-----')
|
||||
expect(data).to.have.property('file')
|
||||
expect(data).to.have.property('timestamp')
|
||||
|
||||
voddo.once('start', function(data) {
|
||||
debug(' [test] restarted after dl close! (expected) <<<<<-----')
|
||||
|
||||
sinon.assert.calledTwice(ytdlStub.exec)
|
||||
expect(this.clock.countTimers()).to.equal(0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
voddo.start()
|
||||
|
||||
clock.next()
|
||||
clock.next()
|
||||
clock.next()
|
||||
clock.next()
|
||||
clock.next()
|
||||
|
||||
})
|
||||
|
||||
})
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,123 @@
|
|||
import 'dotenv/config'
|
||||
import chai, { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import sinonChai from 'sinon-chai'
|
||||
import { CID } from 'multiformats/cid'
|
||||
import EventEmitter from 'node:events'
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'node:path'
|
||||
import postgres from 'postgres'
|
||||
import Capture from '../src/Capture.js'
|
||||
import Voddo from '../src/Voddo.js'
|
||||
import Video from '../src/Video.js'
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
if (typeof process.env.POSTGRES_PASSWORD === 'undefined') throw new Error('missing POSTGRES_PASSWORD');
|
||||
if (typeof process.env.POSTGRES_USERNAME === 'undefined') throw new Error('missing POSTGRES_USERNAME');
|
||||
|
||||
|
||||
const cidFixture = 'bafybeid3mg5lzrvnmpfi5ftwhiupp7i5bgkmdo7dnlwrvklbv33telrrry'
|
||||
const inputFixture = 'projektmelody 3021-10-16 06-16.mp4'
|
||||
const outputFixture = 'projektmelody-chaturbate-30211016T000000Z.mp4'
|
||||
const timestampFixture = 33191316900000
|
||||
|
||||
describe('Capture integration', function () {
|
||||
|
||||
let clock
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers({
|
||||
toFake: ["setTimeout", "setInterval"],
|
||||
shouldAdvanceTime: false
|
||||
});
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
it('end of stream behavior', async function() {
|
||||
const ipfsClusterUpload = sinon.mock()
|
||||
.withExactArgs(outputFixture)
|
||||
.resolves(cidFixture)
|
||||
|
||||
const sql = postgres({
|
||||
username: process.env.POSTGRES_USERNAME,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
host: process.env.POSTGRES_HOST,
|
||||
database: 'futureporn',
|
||||
idle_timeout: 1
|
||||
})
|
||||
|
||||
|
||||
const voddo = sinon.createStubInstance(Voddo)
|
||||
voddo.on.callThrough()
|
||||
voddo.off.callThrough()
|
||||
voddo.emit.callThrough()
|
||||
voddo.listeners.callThrough()
|
||||
voddo.listenerCount.callThrough()
|
||||
voddo.getFilenames.returns([{
|
||||
timestamp: timestampFixture,
|
||||
filename: inputFixture
|
||||
}])
|
||||
|
||||
const video = sinon.createStubInstance(Video)
|
||||
video.concat.resolves(outputFixture)
|
||||
|
||||
const capture = new Capture({
|
||||
voddo,
|
||||
sql,
|
||||
ipfsClusterUpload,
|
||||
video
|
||||
})
|
||||
|
||||
capture.download()
|
||||
voddo.emit('stop', {
|
||||
reason: 'close',
|
||||
stats: {
|
||||
filenames: [
|
||||
inputFixture
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
clock.next() // actionTimer elapse
|
||||
|
||||
|
||||
expect(clock.countTimers()).to.equal(0)
|
||||
clock.restore()
|
||||
|
||||
// gotta wait to verify otherwise verification
|
||||
// occurs before ipfsClusterUpload has a chance
|
||||
// to be invoked.
|
||||
//
|
||||
// (not sure why)
|
||||
//
|
||||
// maybe we're waiting for the
|
||||
// concat promise to resolve?
|
||||
|
||||
await Promise.resolve(() => {
|
||||
expect(ipfsClusterUpload).calledOnce
|
||||
})
|
||||
|
||||
// Capture.save is called as a side effect
|
||||
// of Capture.process
|
||||
// which is called as a side effect of Capture.download
|
||||
// so we have to wait for it to complete
|
||||
// this is not ideal because there is potential
|
||||
// to not wait long enough
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000)
|
||||
})
|
||||
|
||||
const rows = await sql`SELECT "videoSrcHash" FROM vod WHERE "videoSrcHash" = ${cidFixture}`
|
||||
|
||||
expect(rows[0]).to.exist
|
||||
expect(rows[0]).to.have.property('videoSrcHash', cidFixture)
|
||||
|
||||
|
||||
|
||||
})
|
||||
})
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
import Ipfs from '../src/Ipfs.js'
|
||||
import { expect } from 'chai'
|
||||
import path, { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const ipfsExecutable = '/home/chris/.local/bin/ipfs'
|
||||
|
||||
describe('Ipfs', function() {
|
||||
describe('hash', function () {
|
||||
it('should hash a file and return the v1 CID', async function () {
|
||||
const ipfs = new Ipfs({ ipfsExecutable })
|
||||
const cid = await ipfs.hash(path.join(__dirname, '../test/fixtures/mock-stream0.mp4'))
|
||||
expect(cid).to.equal('bafkreihfbftehabfrakhr6tmbx72inewwpayw6cypwgm6lbhbf7mxm7wni')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,62 @@
|
|||
import 'dotenv/config'
|
||||
import Voddo from '../src/Voddo.js'
|
||||
import {
|
||||
expect
|
||||
} from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import YoutubeDlWrap from 'youtube-dl-wrap'
|
||||
import {
|
||||
EventEmitter
|
||||
} from 'events'
|
||||
import { getRandomRoom } from '../src/cb.js'
|
||||
import path, { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
|
||||
describe('voddo', function() {
|
||||
|
||||
|
||||
describe('getVideoLength', function () {
|
||||
it('should return the video length in ms', async function () {
|
||||
const fixtureFile = path.join(__dirname, '..', 'test', 'fixtures', 'mock-stream0.mp4')
|
||||
const length = await Voddo.getVideoLength(fixtureFile)
|
||||
expect(length).to.equal(3819)
|
||||
})
|
||||
})
|
||||
|
||||
it('aborted stream', function(done) {
|
||||
this.timeout(10000)
|
||||
|
||||
getRandomRoom().then((room) => {
|
||||
console.log(room)
|
||||
const abortController = new AbortController()
|
||||
|
||||
const url = `https://chaturbate.com/${room}`
|
||||
const format = 'best'
|
||||
const cwd = '/tmp'
|
||||
const voddo = new Voddo({
|
||||
url,
|
||||
format,
|
||||
cwd
|
||||
})
|
||||
|
||||
|
||||
voddo.once('stop', function(data) {
|
||||
console.log('f in chat')
|
||||
expect(voddo.stats.files[0]).to.have.property('size')
|
||||
done()
|
||||
})
|
||||
|
||||
voddo.start()
|
||||
|
||||
setTimeout(() => {
|
||||
voddo.stop()
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
|
@ -0,0 +1,35 @@
|
|||
import { record, assertDependencyDirectory } from '../../src/record.js'
|
||||
import { getRandomRoom } from '../../src/cb.js'
|
||||
import path from 'node:path'
|
||||
import os from 'node:os'
|
||||
import { execa } from 'execa'
|
||||
|
||||
describe('record', function() {
|
||||
it('should record a file to disk', async function () {
|
||||
this.timeout(1000*60)
|
||||
const roomName = await getRandomRoom()
|
||||
console.log(`roomName:${roomName}`)
|
||||
const appContext = {
|
||||
env: {
|
||||
FUTUREPORN_WORKDIR: os.tmpdir(),
|
||||
DOWNLOADER_UA: "Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0"
|
||||
},
|
||||
logger: {
|
||||
log: (msg) => { console.log(JSON.stringify(msg)) }
|
||||
}
|
||||
}
|
||||
console.log(appContext)
|
||||
const { stdout } = await execa('yt-dlp', ['-g', `https://chaturbate.com/${roomName}`])
|
||||
const playlistUrl = stdout.trim()
|
||||
console.log(`playlistUrl:${playlistUrl}`)
|
||||
assertDependencyDirectory(appContext)
|
||||
const ffmpegProc = record(appContext, playlistUrl, roomName)
|
||||
// console.log(ffmpegProc)
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
ffmpegProc.kill('SIGINT')
|
||||
resolve()
|
||||
}, 1000*10)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,33 @@
|
|||
|
||||
import 'dotenv/config'
|
||||
import Video from '../src/Video.js'
|
||||
import { expect } from 'chai'
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'node:path'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const dataFixture = [
|
||||
{
|
||||
timestamp: 1,
|
||||
file: 'mock-stream0.mp4'
|
||||
}, {
|
||||
timestamp: 2,
|
||||
file: 'mock-stream1.mp4'
|
||||
}, {
|
||||
timestamp: 3,
|
||||
file: 'mock-stream2.mp4'
|
||||
}
|
||||
]
|
||||
|
||||
xdescribe('video', function () {
|
||||
describe('concat', function () {
|
||||
it('should combine several videos into one', async function() {
|
||||
const cwd = path.join(__dirname, './fixtures')
|
||||
const outputFile = await concat(dataFixture, {
|
||||
cwd
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -4,9 +4,9 @@ export default function NotFound() {
|
|||
return (
|
||||
<div className='section'>
|
||||
<h2 className='title is-2'>404 Not Found</h2>
|
||||
<p>Could not find that stream.</p>
|
||||
<p>Could not find that stream archive.</p>
|
||||
|
||||
<Link href="/s">Return to streams list</Link>
|
||||
<Link href="/s">Return to archive list</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import Pager from "@/components/pager";
|
||||
import StreamsList from "@/components/streams-list";
|
||||
import StreamsTable from '@/components/streams-table';
|
||||
import { getAllStreams, getStreamsForVtuber } from "@/lib/streams";
|
||||
// import { getAllVtubers } from "@/lib/vtubers";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
|
||||
|
||||
export default async function Page() {
|
||||
// const vtubers = await getAllVtubers();
|
||||
// const streams = await getAllStreams();
|
||||
// const streams = await getStreamsForVtuber(1)
|
||||
// const pageSize = 100;
|
||||
// const page = 1;
|
||||
|
||||
|
||||
// export interface IStream {
|
||||
// id: number;
|
||||
// attributes: {
|
||||
// date: string;
|
||||
// archiveStatus: 'good' | 'issue' | 'missing';
|
||||
// vods: IVodsResponse;
|
||||
// cuid: string;
|
||||
// vtuber: IVtuberResponse;
|
||||
// tweet: ITweetResponse;
|
||||
// isChaturbateStream: boolean;
|
||||
// isFanslyStream: boolean;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!vtubers) notFound();
|
||||
// const streams = [
|
||||
// {
|
||||
// "firstName": "Tanner",
|
||||
// "lastName": "Linsley",
|
||||
// "age": 33,
|
||||
// "visits": 100,
|
||||
// "progress": 50,
|
||||
// "status": "Married",
|
||||
// "id": 5,
|
||||
// "attributes": {
|
||||
// date: '2023-10-10T15:18:20.003Z',
|
||||
// archiveStatus: 'missing',
|
||||
// isChaturbateStream: false,
|
||||
// isFanslyStream: true,
|
||||
// vods: {},
|
||||
// cuid: '2983482932384',
|
||||
// vtuber: {},
|
||||
// tweet: '',
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "firstName": "Kevin",
|
||||
// "lastName": "Vandy",
|
||||
// "age": 27,
|
||||
// "visits": 200,
|
||||
// "progress": 100,
|
||||
// "status": "Single",
|
||||
// "id": 3,
|
||||
// "attributes": {
|
||||
// date: '2023-10-10T15:18:20.003Z',
|
||||
// archiveStatus: 'missing',
|
||||
// isChaturbateStream: true,
|
||||
// isFanslyStream: true,
|
||||
// vods: {},
|
||||
// cuid: '29823432384',
|
||||
// vtuber: {},
|
||||
// tweet: '',
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
{/* <pre>
|
||||
<p>here are the streams object</p>
|
||||
<code>
|
||||
{JSON.stringify(streams, null, 2)}
|
||||
</code>
|
||||
</pre> */}
|
||||
|
||||
|
||||
|
||||
<h1 className="title">Stream Archive</h1>
|
||||
<StreamsTable />
|
||||
{/* <StreamsList vtubers={vtubers} page={page} pageSize={pageSize} />
|
||||
<Pager baseUrl="/streams" page={page} pageCount={vtubers.length/pageSize}/> */}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -7,7 +7,7 @@ export interface IArchiveProgressProps {
|
|||
}
|
||||
|
||||
export default async function ArchiveProgress ({ vtuber }: IArchiveProgressProps) {
|
||||
const vods = await getVodsForVtuber(vtuber.id)
|
||||
// const vods = await getVodsForVtuber(vtuber.id)
|
||||
// const streams = await getAllStreamsForVtuber(vtuber.id);
|
||||
// const goodStreams = await getAllStreamsForVtuber(vtuber.id, ['good']);
|
||||
// const issueStreams = await getAllStreamsForVtuber(vtuber.id, ['issue']);
|
||||
|
@ -16,17 +16,21 @@ export default async function ArchiveProgress ({ vtuber }: IArchiveProgressProps
|
|||
|
||||
// // Check if totalStreams is not zero before calculating completedPercentage
|
||||
// const completedPercentage = (totalStreams !== 0) ? Math.round(eligibleStreams / totalStreams * 100) : 0;
|
||||
// return (
|
||||
// <div>
|
||||
// <p className="heading">{eligibleStreams}/{totalStreams} Streams Archived ({completedPercentage}%)</p>
|
||||
// <progress className="progress is-success" value={eligibleStreams} max={totalStreams}>{completedPercentage}%</progress>
|
||||
// </div>
|
||||
// )
|
||||
// @todo
|
||||
|
||||
const completedPercentage = 50
|
||||
const totalStreams = 500
|
||||
const eligibleStreams = 50
|
||||
return (
|
||||
<div>
|
||||
<i><p className="">{(vods) ? vods.data.length : 0} vods</p></i>
|
||||
<p>@todo</p>
|
||||
<p className="heading">{eligibleStreams}/{totalStreams} Streams Archived ({completedPercentage}%)</p>
|
||||
<progress className="progress is-success" value={eligibleStreams} max={totalStreams}>{completedPercentage}%</progress>
|
||||
</div>
|
||||
)
|
||||
// @todo
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <i><p className="">{(vods) ? vods.data.length : 0} vods</p></i>
|
||||
// </div>
|
||||
// )
|
||||
}
|
|
@ -17,7 +17,7 @@ export default function Footer() {
|
|||
<ul>
|
||||
<li><Link href="#top">↑ Top of page</Link></li>
|
||||
<li><Link href="/vt">Vtubers</Link></li>
|
||||
{/* <li><Link href="/streams">Stream Archive</Link></li> */}
|
||||
<li><Link href="/archive">Archive</Link></li>
|
||||
<li><Link href="/about">About</Link></li>
|
||||
<li><Link href="/faq">FAQ</Link></li>
|
||||
<li><Link href="/goals">Goals</Link></li>
|
||||
|
|
|
@ -43,7 +43,7 @@ export default function Navbar() {
|
|||
<div className={`navbar-menu ${isExpanded ? 'is-active' : ''}`} id="navMenu">
|
||||
<div className='navbar-start'>
|
||||
<Link className="navbar-item is-expanded" href="/vt">Vtubers</Link>
|
||||
{/* <Link className="navbar-item is-expanded" href="/streams">Stream Archive</Link> */}
|
||||
<Link className="navbar-item is-expanded" href="/archive">Archive</Link>
|
||||
<Link className="navbar-item is-expanded" href="/about">About</Link>
|
||||
<Link className="navbar-item is-expanded" href="/faq">FAQ</Link>
|
||||
<Link className="navbar-item is-expanded" href="/goals">Goals</Link>
|
||||
|
|
|
@ -8,7 +8,7 @@ export function StreamButton({ stream }: { stream: IStream }) {
|
|||
|
||||
return (
|
||||
<Link
|
||||
href={`/streams/${stream.attributes.cuid}`}
|
||||
href={`/archive/${stream.attributes.cuid}`}
|
||||
className="button is-medium"
|
||||
>
|
||||
<span className="mr-2"><FontAwesomeIcon icon={faCalendar} className="fas fa-calendar" /></span><span>{new Date(stream.attributes.date).toLocaleDateString()}</span>
|
||||
|
|
|
@ -89,13 +89,22 @@ export default function StreamPage({ stream }: IStreamProps) {
|
|||
// </pre>
|
||||
|
||||
// </p>
|
||||
// const platformsList = '???';
|
||||
const { isChaturbateInvite, isFanslyInvite } = stream.attributes.tweet.data.attributes;
|
||||
const platformsArray = [
|
||||
isChaturbateInvite ? 'Chaturbate' : null,
|
||||
isFanslyInvite ? 'Fansly' : null
|
||||
].filter(Boolean);
|
||||
const platformsList = platformsArray.length > 0 ? platformsArray.join(', ') : 'None';
|
||||
// const platformsList = [
|
||||
// stream.attributes.isChaturbateStream ? 'Chaturbate' : null,
|
||||
// stream.attributes.isFanslyStream ? 'Fansly' : null
|
||||
// ].filter(Boolean).join(', ');
|
||||
// platformsList = platformsArray.length > 0 ? platformsArray.join(', ') : 'None';
|
||||
|
||||
// const platformsList = [
|
||||
// (stream.attributes.isChaturbateStream && 'CB'),
|
||||
// (stream.attributes.isFanslyStream && 'Fansly')
|
||||
// ].filter(Boolean).join(', ')
|
||||
|
||||
const platformsList = [
|
||||
(stream.attributes.isChaturbateStream && 'CB'),
|
||||
(stream.attributes.isFanslyStream && 'Fansly')
|
||||
].filter(Boolean).join(', ') || '!!!';
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
@ -110,23 +119,39 @@ export default function StreamPage({ stream }: IStreamProps) {
|
|||
<div className="section columns is-multiline">
|
||||
<div className="column is-half">
|
||||
<div className="box">
|
||||
<h2 className="title is-3">Details</h2>
|
||||
<div className="columns is-multiline">
|
||||
<div className="column is-full">
|
||||
<span><b>Announcement</b> <span><Link target="_blank" href={stream.attributes.tweet.data.attributes.url}><FontAwesomeIcon icon={faXTwitter}></FontAwesomeIcon><FontAwesomeIcon icon={faExternalLinkAlt}></FontAwesomeIcon></Link></span></span><br></br>
|
||||
<span><b>Platform</b> </span><span>{platformsList}</span><br></br>
|
||||
<span><b>UTC Datetime</b> </span><time dateTime={date.toISOString()}>{date.toISOString()}</time><br></br>
|
||||
<span><b>Local Datetime</b> </span><span>{date.toLocaleDateString()} {date.toLocaleTimeString()}</span><br></br>
|
||||
<span><b>Lunar Phase</b> </span><span>{Moon.lunarPhase(date)} {Moon.lunarPhaseEmoji(date, { hemisphere })}</span><br></br>
|
||||
<br></br>
|
||||
{/* <select className="mt-5"
|
||||
value={selectedStatus}
|
||||
onChange={e => setSelectedStatus(e.target.value as Status)}
|
||||
>
|
||||
<option>good</option>
|
||||
<option>issue</option>
|
||||
<option>missing</option>
|
||||
</select> */}
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="is-family-sans-serif">Description</th>
|
||||
<th className="is-family-sans-serif">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* <tr>
|
||||
<td>Announcement</td>
|
||||
<td><Link target="_blank" href={stream.attributes.tweet.data.attributes.url}><FontAwesomeIcon icon={faXTwitter}></FontAwesomeIcon><FontAwesomeIcon icon={faExternalLinkAlt}></FontAwesomeIcon></Link></td>
|
||||
</tr> */}
|
||||
<tr>
|
||||
<td>Platform</td>
|
||||
<td>{platformsList}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>UTC Datetime</td>
|
||||
<td><time dateTime={date.toISOString()}>{date.toISOString()}</time></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Local Datetime</td>
|
||||
<td>{date.toLocaleDateString()} {date.toLocaleTimeString()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Lunar Phase</td>
|
||||
<td>{Moon.lunarPhase(date)} {Moon.lunarPhaseEmoji(date, { hemisphere })}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -150,6 +175,7 @@ export default function StreamPage({ stream }: IStreamProps) {
|
|||
</div>
|
||||
|
||||
|
||||
{stream.attributes.vods.data.length !== 0 &&
|
||||
<div className="section">
|
||||
<h1 className="title">VODs</h1>
|
||||
<table className="table">
|
||||
|
@ -179,7 +205,7 @@ export default function StreamPage({ stream }: IStreamProps) {
|
|||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,194 +1,129 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
keepPreviousData,
|
||||
QueryClient,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
|
||||
import {
|
||||
Column,
|
||||
Table as ReactTable,
|
||||
PaginationState,
|
||||
useReactTable,
|
||||
ColumnFiltersState,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFacetedMinMaxValues,
|
||||
getPaginationRowModel,
|
||||
sortingFns,
|
||||
getSortedRowModel,
|
||||
FilterFn,
|
||||
SortingFn,
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
FilterFns,
|
||||
ColumnOrderState,
|
||||
createColumnHelper,
|
||||
} from '@tanstack/react-table'
|
||||
import Image from 'next/image';
|
||||
import { useState } from "react";
|
||||
import { IStream } from "@/lib/streams";
|
||||
import { LocalizedDate } from './localized-date';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAngleLeft, faAngleRight, faAnglesLeft, faAnglesRight, faChevronCircleRight, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import Link from 'next/link';
|
||||
|
||||
function Filter({
|
||||
column,
|
||||
table,
|
||||
}: {
|
||||
column: Column<any, any>
|
||||
table: ReactTable<any>
|
||||
}) {
|
||||
const firstValue = table
|
||||
.getPreFilteredRowModel()
|
||||
.flatRows[0]?.getValue(column.id)
|
||||
|
||||
const columnFilterValue = column.getFilterValue()
|
||||
|
||||
|
||||
import { fetchStreamData, IStream } from '@/lib/streams'
|
||||
|
||||
if (typeof firstValue === 'number') {
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
value={(columnFilterValue as [number, number])?.[0] ?? ''}
|
||||
onChange={e =>
|
||||
column.setFilterValue((old: [number, number]) => [
|
||||
e.target.value,
|
||||
old?.[1],
|
||||
])
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
function getStatusClass(value: string) {
|
||||
switch (value) {
|
||||
case 'issue':
|
||||
return 'is-warning';
|
||||
case 'missing':
|
||||
return 'is-danger';
|
||||
case 'good':
|
||||
return 'is-success';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
placeholder={`Min`}
|
||||
className="w-24 border shadow rounded"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={(columnFilterValue as [number, number])?.[1] ?? ''}
|
||||
onChange={e =>
|
||||
column.setFilterValue((old: [number, number]) => [
|
||||
old?.[0],
|
||||
e.target.value,
|
||||
])
|
||||
}
|
||||
placeholder={`Max`}
|
||||
className="w-24 border shadow rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
export default function StreamsTable() {
|
||||
const rerender = React.useReducer(() => ({}), {})[1]
|
||||
|
||||
|
||||
// name
|
||||
// title
|
||||
// platform
|
||||
// date
|
||||
// archiveStatus
|
||||
const columns = React.useMemo<ColumnDef<IStream>[]>(
|
||||
() => [
|
||||
{
|
||||
header: 'VTuber',
|
||||
accessorFn: d => d.attributes.vtuber.data?.attributes?.displayName,
|
||||
},
|
||||
{
|
||||
header: 'Date',
|
||||
accessorFn: d => new Date(d.attributes.date2).toISOString().split('T').at(0),
|
||||
cell: info => <Link href={`/archive/${info.row.original.attributes.cuid}`}>{info.getValue() as string}</Link>
|
||||
},
|
||||
{
|
||||
header: 'Platform',
|
||||
accessorFn: d => [
|
||||
(d.attributes.isChaturbateStream && 'CB'),
|
||||
(d.attributes.isFanslyStream && 'Fansly')
|
||||
].filter(Boolean).join(', ') || '???'
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
accessorFn: d => {
|
||||
if (!d.attributes.archiveStatus) return 'missing';
|
||||
return d.attributes.archiveStatus
|
||||
}
|
||||
},
|
||||
// {
|
||||
// header: 'Name',
|
||||
// footer: props => props.column.id,
|
||||
// columns: [
|
||||
// {
|
||||
// accessorKey: 'firstName',
|
||||
// cell: info => info.getValue(),
|
||||
// footer: props => props.column.id,
|
||||
// },
|
||||
// {
|
||||
// accessorFn: row => row.lastName,
|
||||
// id: 'lastName',
|
||||
// cell: info => info.getValue(),
|
||||
// header: () => <span>Last Name</span>,
|
||||
// footer: props => props.column.id,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof firstValue === 'boolean') {
|
||||
return (
|
||||
<>
|
||||
<div className='select'>
|
||||
<select
|
||||
onChange={(evt) => {
|
||||
if (evt.target.value === "any")
|
||||
column?.setFilterValue(null);
|
||||
if (evt.target.value === "yes")
|
||||
column?.setFilterValue(true);
|
||||
if (evt.target.value === "no")
|
||||
column?.setFilterValue(false);
|
||||
}}
|
||||
>
|
||||
<option>any</option>
|
||||
<option>yes</option>
|
||||
<option>no</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={(columnFilterValue ?? '') as string}
|
||||
onChange={e => column.setFilterValue(e.target.value)}
|
||||
placeholder={`Search...`}
|
||||
className="input"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const archiveStatusClassName = (archiveStatus: string): string => {
|
||||
if (archiveStatus === 'missing') return 'is-danger';
|
||||
if (archiveStatus === 'issue') return 'is-warning';
|
||||
if (archiveStatus === 'good') return 'is-success';
|
||||
return 'is-info';
|
||||
};
|
||||
|
||||
export default function StreamsTable({ streams }: { streams: IStream[] }) {
|
||||
|
||||
|
||||
|
||||
const columnHelper = createColumnHelper<IStream>()
|
||||
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor('attributes.cuid', {
|
||||
cell: info => <Link href={`/streams/${info.getValue()}`}>{info.getValue()}</Link>,
|
||||
header: () => <span>ID</span>
|
||||
}),
|
||||
columnHelper.accessor('attributes.vtuber.data.attributes.image', {
|
||||
cell: info => <figure className='image is-24x24'><Image className="is-rounded" width={24} height={24} alt="" src={info.getValue()}></Image></figure>,
|
||||
header: () => <span></span>,
|
||||
enableColumnFilter: false
|
||||
}),
|
||||
columnHelper.accessor('attributes.vtuber.data.attributes.displayName', {
|
||||
cell: info => info.getValue(),
|
||||
header: () => <span>VTuber</span>
|
||||
}),
|
||||
columnHelper.accessor('attributes.date', {
|
||||
cell: info => <LocalizedDate date={new Date(info.getValue())}/>,
|
||||
header: () => <span>Date</span>
|
||||
}),
|
||||
columnHelper.accessor('attributes.isChaturbateStream', {
|
||||
id: 'isChaturbateStream',
|
||||
cell: info => info.getValue() === true ? 'yes' : 'no',
|
||||
header: () => <span>Chaturbate</span>
|
||||
}),
|
||||
columnHelper.accessor('attributes.isFanslyStream', {
|
||||
id: 'isFanslyStream',
|
||||
cell: info => info.getValue() === true ? 'yes' : 'no',
|
||||
header: () => <span>Fansly</span>
|
||||
}),
|
||||
columnHelper.accessor('attributes.archiveStatus', {
|
||||
cell: info => <div className={`tag ${archiveStatusClassName(info.getValue())}`} >{info.getValue()}</div>,
|
||||
header: () => <span>Status</span>
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 50,
|
||||
})
|
||||
]
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState({})
|
||||
const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([])
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||
const [data, setData] = useState(() => streams)
|
||||
const dataQuery = useQuery({
|
||||
queryKey: ['streams', pagination.pageIndex, pagination.pageSize],
|
||||
queryFn: () => fetchStreamData(pagination),
|
||||
placeholderData: keepPreviousData, // don't have 0 rows flash while changing pages/loading next page,
|
||||
staleTime: 1000
|
||||
}, queryClient)
|
||||
|
||||
const defaultData = React.useMemo(() => [], [])
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
data: dataQuery?.data?.rows ?? defaultData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
// pageCount: dataQuery.data?.pageCount ?? -1, //you can now pass in `rowCount` instead of pageCount and `pageCount` will be calculated internally (new in v8.13.0)
|
||||
rowCount: dataQuery.data?.rowCount, // new in v8.13.0 - alternatively, just pass in `pageCount` directly
|
||||
state: {
|
||||
columnVisibility,
|
||||
columnOrder,
|
||||
columnFilters
|
||||
pagination,
|
||||
},
|
||||
onColumnOrderChange: setColumnOrder,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true, //we're doing manual "server-side" pagination
|
||||
// getPaginationRowModel: getPaginationRowModel(), // If only doing manual pagination, you don't need this
|
||||
debugTable: true,
|
||||
})
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
|
||||
<div className="p-2">
|
||||
<div className="h-2" />
|
||||
|
||||
|
||||
|
||||
<table className='table'>
|
||||
<table className='table is-hoverable is-fullwidth'>
|
||||
<thead>
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
|
@ -201,11 +136,6 @@ export default function StreamsTable({ streams }: { streams: IStream[] }) {
|
|||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{header.column.getCanFilter() ? (
|
||||
<div>
|
||||
<Filter column={header.column} table={table} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
|
@ -220,7 +150,10 @@ export default function StreamsTable({ streams }: { streams: IStream[] }) {
|
|||
<tr key={row.id}>
|
||||
{row.getVisibleCells().map(cell => {
|
||||
return (
|
||||
<td key={cell.id}>
|
||||
<td
|
||||
className={getStatusClass(cell.getValue() as string)}
|
||||
key={cell.id}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
|
@ -233,52 +166,53 @@ export default function StreamsTable({ streams }: { streams: IStream[] }) {
|
|||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="columns is-multiline is-mobile" />
|
||||
<div className="column is-12">
|
||||
|
||||
<div className="columns is-mobile is-vcentered">
|
||||
<div className='column is-half'>
|
||||
<button
|
||||
className="button icon is-rounded is-medium p-1 m-1"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
className="button border rounded mx-1"
|
||||
onClick={() => table.firstPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<FontAwesomeIcon className='fa-solid fa-angles-left' icon={faAnglesLeft}></FontAwesomeIcon>
|
||||
{'<<'}
|
||||
</button>
|
||||
<button
|
||||
className="button icon is-rounded is-medium p-1 m-1"
|
||||
className="button border rounded mx-1"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<FontAwesomeIcon className='fa-solid fa-angle-left' icon={faAngleLeft}></FontAwesomeIcon>
|
||||
{'<'}
|
||||
</button>
|
||||
<button
|
||||
className="button icon is-rounded is-medium p-1 m-1"
|
||||
className="button border rounded mx-1"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<FontAwesomeIcon className="fa-solid fa-angle-right" icon={faAngleRight}></FontAwesomeIcon>
|
||||
{'>'}
|
||||
</button>
|
||||
<button
|
||||
className="button icon is-medium is-rounded p-1 m-1"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
className="button border rounded mx-1"
|
||||
onClick={() => table.lastPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<FontAwesomeIcon className='fa-solid fa-angles-right' icon={faAnglesRight}></FontAwesomeIcon>
|
||||
{'>>'}
|
||||
</button>
|
||||
</div>
|
||||
<div className='column is-2'>
|
||||
|
||||
<div className=''>
|
||||
<div className=''>
|
||||
<span className='mr-1'>Page</span>
|
||||
<div className='column is-half'>
|
||||
<span>Page </span>
|
||||
<strong>
|
||||
{table.getState().pagination.pageIndex + 1} of{' '}
|
||||
{table.getPageCount()}
|
||||
{table.getPageCount().toLocaleString()}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className=''>
|
||||
<label className='label'>
|
||||
Go to page:
|
||||
</label>
|
||||
<div className="control is-expanded">
|
||||
{/* second row with page number input and pages-per-screen select */}
|
||||
<div className='columns is-mobile is-vcentered'>
|
||||
<div className='column is-2 '>
|
||||
<span className='is-text-centered'>Go to page:</span>
|
||||
</div>
|
||||
<div className='column is-3'>
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={table.getState().pagination.pageIndex + 1}
|
||||
|
@ -286,24 +220,18 @@ export default function StreamsTable({ streams }: { streams: IStream[] }) {
|
|||
const page = e.target.value ? Number(e.target.value) - 1 : 0
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
className="input p-1"
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='column is-2'>
|
||||
<div className="m-1">
|
||||
<span className='mr-1'>Page</span>
|
||||
</div>
|
||||
<div className='select'>
|
||||
<div className='column is-5'>
|
||||
<div className="select">
|
||||
<select
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={e => {
|
||||
table.setPageSize(Number(e.target.value))
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map(pageSize => (
|
||||
{[20, 50, 100].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Show {pageSize}
|
||||
</option>
|
||||
|
@ -313,6 +241,9 @@ export default function StreamsTable({ streams }: { streams: IStream[] }) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
|
|
@ -156,9 +156,9 @@ export function Tagger({ vod, setTimestamps }: ITaggerProps): React.JSX.Element
|
|||
}
|
||||
}
|
||||
|
||||
if (!isAuthed) {
|
||||
return <></>
|
||||
} else {
|
||||
// if (!isAuthed) {
|
||||
// return <></>
|
||||
// } else {
|
||||
if (isEditor) {
|
||||
return (
|
||||
<div className='card mt-2' style={{ width: '100%' }}>
|
||||
|
@ -234,7 +234,7 @@ export function Tagger({ vod, setTimestamps }: ITaggerProps): React.JSX.Element
|
|||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -110,7 +110,6 @@ export const VideoPlayer = forwardRef(function VideoPlayer( props: IPlayerProps,
|
|||
|
||||
return (
|
||||
<>
|
||||
<p className='notification'>CDN1 (for Patrons only)</p>
|
||||
<MuxPlayer
|
||||
onCanPlay={() => {
|
||||
setIsPlayerReady(true)}
|
||||
|
|
|
@ -31,12 +31,16 @@ export function VideoSourceSelector({
|
|||
if (isEntitledToCDN) {
|
||||
if (selectedVideoSource === 'Mux' && isMux) {
|
||||
return 'Mux';
|
||||
} else if (selectedVideoSource === 'B2' && isB2) {
|
||||
return 'B2';
|
||||
}
|
||||
}
|
||||
// If the user doesn't have entitlements or their preference is not available, default to IPFS
|
||||
if (isIPFSSource) {
|
||||
|
||||
// if the user has B2 as their preference or they have no preference, use B2
|
||||
if (selectedVideoSource === 'B2' || !selectedVideoSource) {
|
||||
return 'B2'
|
||||
}
|
||||
|
||||
// use IPFS only if the user has opted to use it
|
||||
if (selectedVideoSource === 'IPFSSource' && isIPFSSource) {
|
||||
return 'IPFSSource';
|
||||
} else if (isIPFS240) {
|
||||
return 'IPFS240';
|
||||
|
@ -53,8 +57,16 @@ export function VideoSourceSelector({
|
|||
// Check if the saved preference is valid based on entitlements and available sources
|
||||
if (savedPreference === 'Mux' && isMux && isEntitledToCDN) {
|
||||
setSelectedVideoSource('Mux');
|
||||
} else if (savedPreference === 'B2' && isB2 && isEntitledToCDN) {
|
||||
} else if (savedPreference === 'B2') {
|
||||
setSelectedVideoSource('B2');
|
||||
} else if (savedPreference === 'IPFSSource') {
|
||||
setSelectedVideoSource('IPFSSource');
|
||||
} else if (savedPreference === 'IPFS240') {
|
||||
if (isIPFS240) {
|
||||
setSelectedVideoSource('IPFS240');
|
||||
} else {
|
||||
setSelectedVideoSource('IPFSSource');
|
||||
}
|
||||
} else {
|
||||
// Determine the best video source if the saved preference is invalid or not available
|
||||
const bestSource = determineBestVideoSource();
|
||||
|
@ -69,7 +81,7 @@ export function VideoSourceSelector({
|
|||
const handleSourceClick = (source: string) => {
|
||||
if (
|
||||
(source === 'Mux' && isMux && isEntitledToCDN) ||
|
||||
(source === 'B2' && isB2 && isEntitledToCDN) ||
|
||||
(source === 'B2' && isB2) ||
|
||||
(source === 'IPFSSource') ||
|
||||
(source === 'IPFS240')
|
||||
) {
|
||||
|
@ -100,9 +112,9 @@ export function VideoSourceSelector({
|
|||
</button>
|
||||
</div>}
|
||||
{(isB2) && <div className="nav-item">
|
||||
<button onClick={() => handleSourceClick('B2')} disabled={!isEntitledToCDN} className={`button ${selectedVideoSource === 'B2' && 'is-active'}`}>
|
||||
<button onClick={() => handleSourceClick('B2')} className={`button ${selectedVideoSource === 'B2' && 'is-active'}`}>
|
||||
<span className="icon">
|
||||
<FontAwesomeIcon icon={faPatreon} className="fab fa-patreon" />
|
||||
<FontAwesomeIcon icon={faGlobe} className="fab fa-globe" />
|
||||
</span>
|
||||
<span>CDN 2</span>
|
||||
</button>
|
||||
|
|
|
@ -7,12 +7,17 @@ import "@fortawesome/fontawesome-svg-core/styles.css";
|
|||
import { AuthProvider } from './components/auth';
|
||||
import type { Metadata } from 'next';
|
||||
import NotificationCenter from './components/notification-center';
|
||||
import UppyProvider from './uppy';
|
||||
// import {
|
||||
// QueryClientProvider,
|
||||
// QueryClient
|
||||
// } from '@tanstack/react-query'
|
||||
// import NextTopLoader from 'nextjs-toploader';
|
||||
// import Ipfs from './components/ipfs'; // slows down the page too much
|
||||
|
||||
|
||||
|
||||
// const queryClient = new QueryClient()
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Futureporn.net',
|
||||
description: "The Galaxy's Best VTuber Hentai Site",
|
||||
|
@ -56,14 +61,14 @@ export default function RootLayout({
|
|||
shadow="0 0 10px #2299DD,0 0 5px #2299DD"
|
||||
/> */}
|
||||
<AuthProvider>
|
||||
<UppyProvider>
|
||||
{/* <QueryClientProvider client={queryClient}> */}
|
||||
<Navbar />
|
||||
<NotificationCenter />
|
||||
<div className="container">
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
</UppyProvider>
|
||||
{/* </QueryClientProvider> */}
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -29,6 +29,6 @@ export default async function fetchAPI(
|
|||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error(`Error while fetching data from API.`);
|
||||
throw new Error(`Error while fetching data from API. ${path}`);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { strapiUrl, siteUrl } from './constants';
|
||||
import { siteUrl, strapiUrl } from './constants';
|
||||
import { getSafeDate } from './dates';
|
||||
import { IVodsResponse } from './vods';
|
||||
import { IVtuber, IVtuberResponse } from './vtubers';
|
||||
|
@ -12,6 +12,7 @@ export interface IStream {
|
|||
id: number;
|
||||
attributes: {
|
||||
date: string;
|
||||
date2: string;
|
||||
archiveStatus: 'good' | 'issue' | 'missing';
|
||||
vods: IVodsResponse;
|
||||
cuid: string;
|
||||
|
@ -35,7 +36,8 @@ export interface IStreamsResponse {
|
|||
|
||||
const fetchStreamsOptions = {
|
||||
next: {
|
||||
tags: ['streams']
|
||||
tags: ['streams'],
|
||||
revalidation: 1
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,6 +164,7 @@ export async function getStream(id: number): Promise<IStream> {
|
|||
|
||||
|
||||
export async function getAllStreams(archiveStatuses = ['missing', 'issue', 'good']): Promise<IStream[]> {
|
||||
throw new Error('getAllStreams function is not performant. please use something more efficient.')
|
||||
const pageSize = 100; // Adjust this value as needed
|
||||
const sortDesc = true; // Adjust the sorting direction as needed
|
||||
|
||||
|
@ -308,7 +311,50 @@ export async function getAllStreamsForVtuber(vtuberId: number, archiveStatuses =
|
|||
return allStreams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used as table data on /archive page.
|
||||
* .pageIndex, pagination.pageSize
|
||||
*/
|
||||
|
||||
export async function fetchStreamData({ pageIndex, pageSize }: { pageIndex: number, pageSize: number }) {
|
||||
const offset = pageIndex * pageSize;
|
||||
const query = qs.stringify({
|
||||
populate: {
|
||||
vtuber: {
|
||||
fields: ['slug', 'displayName', 'publishedAt']
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
vtuber: {
|
||||
publishedAt: {
|
||||
$notNull: true
|
||||
}
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
start: offset,
|
||||
limit: pageSize,
|
||||
withCount: true
|
||||
}
|
||||
})
|
||||
const response = await fetch(
|
||||
`${strapiUrl}/api/streams?${query}`
|
||||
);
|
||||
const json = await response.json();
|
||||
console.log(json)
|
||||
const d = {
|
||||
rows: json.data,
|
||||
pageCount: Math.ceil(json.meta.pagination.total / pageSize),
|
||||
rowCount: json.meta.pagination.total,
|
||||
}
|
||||
// console.log(`fetchStreamData with pageIndex=${pageIndex}, pageSize=${pageSize}\n\n${JSON.stringify(d, null, 2)}`)
|
||||
return d;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function getStreamsForVtuber(vtuberId: number, page: number = 1, pageSize: number = 25, sortDesc = true): Promise<IStreamsResponse> {
|
||||
console.log(`getStreamsForVtuber() with strapiUrl=${strapiUrl}`)
|
||||
const query = qs.stringify(
|
||||
{
|
||||
populate: {
|
||||
|
@ -334,8 +380,10 @@ export async function getStreamsForVtuber(vtuberId: number, page: number = 1, pa
|
|||
}
|
||||
}
|
||||
)
|
||||
return fetch(`${strapiUrl}/api/streams?${query}`, fetchStreamsOptions)
|
||||
.then((res) => res.json())
|
||||
const res = await fetch(`${strapiUrl}/api/streams?${query}`, fetchStreamsOptions)
|
||||
const data = await res.json()
|
||||
console.log(data)
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import Pager from "@/components/pager";
|
||||
import StreamsList from "@/components/streams-list";
|
||||
import StreamsTable from '@/components/streams-table';
|
||||
import { getAllStreams } from "@/lib/streams";
|
||||
// import { getAllVtubers } from "@/lib/vtubers";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export default async function Page() {
|
||||
// const vtubers = await getAllVtubers();
|
||||
const streams = await getAllStreams();
|
||||
// const pageSize = 100;
|
||||
// const page = 1;
|
||||
|
||||
// if (!vtubers) notFound();
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
{/* <pre>
|
||||
<code>
|
||||
{JSON.stringify(vtubers, null, 2)}
|
||||
</code>
|
||||
</pre> */}
|
||||
|
||||
<h1 className="title">Stream Archive</h1>
|
||||
<StreamsTable streams={streams} />
|
||||
{/* <StreamsList vtubers={vtubers} page={page} pageSize={pageSize} />
|
||||
<Pager baseUrl="/streams" page={page} pageCount={vtubers.length/pageSize}/> */}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -22,6 +22,7 @@
|
|||
"@mux/mux-player-react": "^2.4.1",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@react-hookz/web": "^24.0.4",
|
||||
"@tanstack/react-query": "^5.32.1",
|
||||
"@tanstack/react-table": "^8.15.3",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@types/qs": "^6.9.14",
|
||||
|
@ -53,12 +54,14 @@
|
|||
"prism-react-renderer": "^2.3.1",
|
||||
"qs": "^6.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-data-table-component": "^7.5.4",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.2",
|
||||
"react-loading-skeleton": "^3.4.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
"sharp": "^0.33.3",
|
||||
"slugify": "^1.6.6",
|
||||
"styled-components": "5.3.3",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -41,6 +41,9 @@ dependencies:
|
|||
'@react-hookz/web':
|
||||
specifier: ^24.0.4
|
||||
version: 24.0.4(react-dom@18.2.0)(react@18.2.0)
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.32.1
|
||||
version: 5.32.1(react@18.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.15.3
|
||||
version: 8.15.3(react-dom@18.2.0)(react@18.2.0)
|
||||
|
@ -115,7 +118,7 @@ dependencies:
|
|||
version: 13.1.0
|
||||
next:
|
||||
specifier: 14.0.4
|
||||
version: 14.0.4(react-dom@18.2.0)(react@18.2.0)
|
||||
version: 14.0.4(@babel/core@7.24.5)(react-dom@18.2.0)(react@18.2.0)
|
||||
next-goatcounter:
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5(next@14.0.4)(react-dom@18.2.0)(react@18.2.0)
|
||||
|
@ -134,6 +137,9 @@ dependencies:
|
|||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
react-data-table-component:
|
||||
specifier: ^7.5.4
|
||||
version: 7.5.4(react@18.2.0)(styled-components@5.3.3)
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
|
@ -152,6 +158,9 @@ dependencies:
|
|||
slugify:
|
||||
specifier: ^1.6.6
|
||||
version: 1.6.6
|
||||
styled-components:
|
||||
specifier: 5.3.3
|
||||
version: 5.3.3(@babel/core@7.24.5)(react-dom@18.2.0)(react-is@16.13.1)(react@18.2.0)
|
||||
yup:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
|
@ -180,12 +189,234 @@ packages:
|
|||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/@ampproject/remapping@2.3.0:
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.5
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
dev: false
|
||||
|
||||
/@babel/code-frame@7.24.2:
|
||||
resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/highlight': 7.24.5
|
||||
picocolors: 1.0.0
|
||||
dev: false
|
||||
|
||||
/@babel/compat-data@7.24.4:
|
||||
resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/core@7.24.5:
|
||||
resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@babel/code-frame': 7.24.2
|
||||
'@babel/generator': 7.24.5
|
||||
'@babel/helper-compilation-targets': 7.23.6
|
||||
'@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5)
|
||||
'@babel/helpers': 7.24.5
|
||||
'@babel/parser': 7.24.5
|
||||
'@babel/template': 7.24.0
|
||||
'@babel/traverse': 7.24.5(supports-color@5.5.0)
|
||||
'@babel/types': 7.24.5
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@babel/generator@7.24.5:
|
||||
resolution: {integrity: sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.24.5
|
||||
'@jridgewell/gen-mapping': 0.3.5
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
jsesc: 2.5.2
|
||||
dev: false
|
||||
|
||||
/@babel/helper-annotate-as-pure@7.22.5:
|
||||
resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.24.5
|
||||
dev: false
|
||||
|
||||
/@babel/helper-compilation-targets@7.23.6:
|
||||
resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.24.4
|
||||
'@babel/helper-validator-option': 7.23.5
|
||||
browserslist: 4.23.0
|
||||
lru-cache: 5.1.1
|
||||
semver: 6.3.1
|
||||
dev: false
|
||||
|
||||
/@babel/helper-environment-visitor@7.22.20:
|
||||
resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/helper-function-name@7.23.0:
|
||||
resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/template': 7.24.0
|
||||
'@babel/types': 7.24.5
|
||||
dev: false
|
||||
|
||||
/@babel/helper-hoist-variables@7.22.5:
|
||||
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.24.5
|
||||
dev: false
|
||||
|
||||
/@babel/helper-module-imports@7.24.3:
|
||||
resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.24.5
|
||||
dev: false
|
||||
|
||||
/@babel/helper-module-transforms@7.24.5(@babel/core@7.24.5):
|
||||
resolution: {integrity: sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
dependencies:
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-environment-visitor': 7.22.20
|
||||
'@babel/helper-module-imports': 7.24.3
|
||||
'@babel/helper-simple-access': 7.24.5
|
||||
'@babel/helper-split-export-declaration': 7.24.5
|
||||
'@babel/helper-validator-identifier': 7.24.5
|
||||
dev: false
|
||||
|
||||
/@babel/helper-plugin-utils@7.24.5:
|
||||
resolution: {integrity: sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/helper-simple-access@7.24.5:
|
||||
resolution: {integrity: sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.24.5
|
||||
dev: false
|
||||
|
||||
/@babel/helper-split-export-declaration@7.24.5:
|
||||
resolution: {integrity: sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.24.5
|
||||
dev: false
|
||||
|
||||
/@babel/helper-string-parser@7.24.1:
|
||||
resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/helper-validator-identifier@7.24.5:
|
||||
resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/helper-validator-option@7.23.5:
|
||||
resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/@babel/helpers@7.24.5:
|
||||
resolution: {integrity: sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/template': 7.24.0
|
||||
'@babel/traverse': 7.24.5(supports-color@5.5.0)
|
||||
'@babel/types': 7.24.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@babel/highlight@7.24.5:
|
||||
resolution: {integrity: sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.24.5
|
||||
chalk: 2.4.2
|
||||
js-tokens: 4.0.0
|
||||
picocolors: 1.0.0
|
||||
dev: false
|
||||
|
||||
/@babel/parser@7.24.5:
|
||||
resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.24.5
|
||||
dev: false
|
||||
|
||||
/@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.5):
|
||||
resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.24.5
|
||||
'@babel/helper-plugin-utils': 7.24.5
|
||||
dev: false
|
||||
|
||||
/@babel/runtime@7.24.4:
|
||||
resolution: {integrity: sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
/@babel/template@7.24.0:
|
||||
resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.24.2
|
||||
'@babel/parser': 7.24.5
|
||||
'@babel/types': 7.24.5
|
||||
dev: false
|
||||
|
||||
/@babel/traverse@7.24.5(supports-color@5.5.0):
|
||||
resolution: {integrity: sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.24.2
|
||||
'@babel/generator': 7.24.5
|
||||
'@babel/helper-environment-visitor': 7.22.20
|
||||
'@babel/helper-function-name': 7.23.0
|
||||
'@babel/helper-hoist-variables': 7.22.5
|
||||
'@babel/helper-split-export-declaration': 7.24.5
|
||||
'@babel/parser': 7.24.5
|
||||
'@babel/types': 7.24.5
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@babel/types@7.24.5:
|
||||
resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.24.1
|
||||
'@babel/helper-validator-identifier': 7.24.5
|
||||
to-fast-properties: 2.0.0
|
||||
dev: false
|
||||
|
||||
/@emnapi/runtime@1.1.1:
|
||||
resolution: {integrity: sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==}
|
||||
requiresBuild: true
|
||||
|
@ -194,6 +425,24 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@emotion/is-prop-valid@0.8.8:
|
||||
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
|
||||
dependencies:
|
||||
'@emotion/memoize': 0.7.4
|
||||
dev: false
|
||||
|
||||
/@emotion/memoize@0.7.4:
|
||||
resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
|
||||
dev: false
|
||||
|
||||
/@emotion/stylis@0.8.5:
|
||||
resolution: {integrity: sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==}
|
||||
dev: false
|
||||
|
||||
/@emotion/unitless@0.7.5:
|
||||
resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
|
||||
dev: false
|
||||
|
||||
/@eslint-community/eslint-utils@4.4.0(eslint@8.57.0):
|
||||
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
@ -214,7 +463,7 @@ packages:
|
|||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.3.4
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
espree: 9.6.1
|
||||
globals: 13.24.0
|
||||
ignore: 5.3.1
|
||||
|
@ -303,7 +552,7 @@ packages:
|
|||
engines: {node: '>=10.10.0'}
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
debug: 4.3.4
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
@ -506,6 +755,36 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@jridgewell/gen-mapping@0.3.5:
|
||||
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.2.1
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
dev: false
|
||||
|
||||
/@jridgewell/resolve-uri@3.1.2:
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dev: false
|
||||
|
||||
/@jridgewell/set-array@1.2.1:
|
||||
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dev: false
|
||||
|
||||
/@jridgewell/sourcemap-codec@1.4.15:
|
||||
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
|
||||
dev: false
|
||||
|
||||
/@jridgewell/trace-mapping@0.3.25:
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: false
|
||||
|
||||
/@mux/blurhash@0.1.2:
|
||||
resolution: {integrity: sha512-fWLOXHS2l3CGyPHF4NSRLqZx9DDAz1WYC4YXD3du24xxibIKUyBYzg7PDtx54z5QaQ12ln5oPFvhH5LhaLzeZg==}
|
||||
dependencies:
|
||||
|
@ -712,6 +991,19 @@ packages:
|
|||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@tanstack/query-core@5.32.1:
|
||||
resolution: {integrity: sha512-mCWa1wdGb1jiny4+qYegbSeadcFj+Nq65KFSs4A1DRveoIq7SrTwUhqu7hrB6d54cQH5x59DfJvxusn3w1Cj/g==}
|
||||
dev: false
|
||||
|
||||
/@tanstack/react-query@5.32.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-+nXLMB0JK0XwTJ+lQt49DPNLrbSppni9N5W5yMR085yW3YaRKRUFhfVTER3TvQd1UycHpoGPFnt1gHiijXERAg==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.32.1
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@tanstack/react-table@8.15.3(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-aocQ4WpWiAh7R+yxNp+DGQYXeVACh5lv2kk96DjYgFiHDCB0cOFoYMT/pM6eDOzeMXR9AvPoLeumTgq8/0qX+w==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -794,7 +1086,7 @@ packages:
|
|||
'@typescript-eslint/types': 6.21.0
|
||||
'@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3)
|
||||
'@typescript-eslint/visitor-keys': 6.21.0
|
||||
debug: 4.3.4
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
eslint: 8.57.0
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
|
@ -825,7 +1117,7 @@ packages:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 6.21.0
|
||||
'@typescript-eslint/visitor-keys': 6.21.0
|
||||
debug: 4.3.4
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.3
|
||||
|
@ -1198,6 +1490,13 @@ packages:
|
|||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/ansi-styles@3.2.1:
|
||||
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
color-convert: 1.9.3
|
||||
dev: false
|
||||
|
||||
/ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -1353,6 +1652,21 @@ packages:
|
|||
dequal: 2.0.3
|
||||
dev: true
|
||||
|
||||
/babel-plugin-styled-components@2.1.4(@babel/core@7.24.5)(styled-components@5.3.3):
|
||||
resolution: {integrity: sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==}
|
||||
peerDependencies:
|
||||
styled-components: '>= 2'
|
||||
dependencies:
|
||||
'@babel/helper-annotate-as-pure': 7.22.5
|
||||
'@babel/helper-module-imports': 7.24.3
|
||||
'@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.5)
|
||||
lodash: 4.17.21
|
||||
picomatch: 2.3.1
|
||||
styled-components: 5.3.3(@babel/core@7.24.5)(react-dom@18.2.0)(react-is@16.13.1)(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
dev: false
|
||||
|
||||
/balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
dev: true
|
||||
|
@ -1397,6 +1711,17 @@ packages:
|
|||
dependencies:
|
||||
fill-range: 7.0.1
|
||||
|
||||
/browserslist@4.23.0:
|
||||
resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001607
|
||||
electron-to-chromium: 1.4.752
|
||||
node-releases: 2.0.14
|
||||
update-browserslist-db: 1.0.13(browserslist@4.23.0)
|
||||
dev: false
|
||||
|
||||
/buffer@5.7.1:
|
||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||
dependencies:
|
||||
|
@ -1432,6 +1757,10 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/camelize@1.0.1:
|
||||
resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
|
||||
dev: false
|
||||
|
||||
/caniuse-lite@1.0.30001607:
|
||||
resolution: {integrity: sha512-WcvhVRjXLKFB/kmOFVwELtMxyhq3iM/MvmXcyCe2PNf166c39mptscOc/45TTS96n2gpNV2z7+NakArTWZCQ3w==}
|
||||
dev: false
|
||||
|
@ -1442,6 +1771,15 @@ packages:
|
|||
custom-media-element: 1.2.3
|
||||
dev: false
|
||||
|
||||
/chalk@2.4.2:
|
||||
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
ansi-styles: 3.2.1
|
||||
escape-string-regexp: 1.0.5
|
||||
supports-color: 5.5.0
|
||||
dev: false
|
||||
|
||||
/chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -1487,12 +1825,22 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/color-convert@1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
dependencies:
|
||||
color-name: 1.1.3
|
||||
dev: false
|
||||
|
||||
/color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
/color-name@1.1.3:
|
||||
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||
dev: false
|
||||
|
||||
/color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
|
@ -1515,6 +1863,10 @@ packages:
|
|||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
dev: true
|
||||
|
||||
/convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
dev: false
|
||||
|
||||
/core-js@3.36.1:
|
||||
resolution: {integrity: sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==}
|
||||
requiresBuild: true
|
||||
|
@ -1529,6 +1881,19 @@ packages:
|
|||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/css-color-keywords@1.0.0:
|
||||
resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/css-to-react-native@3.2.0:
|
||||
resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==}
|
||||
dependencies:
|
||||
camelize: 1.0.1
|
||||
css-color-keywords: 1.0.0
|
||||
postcss-value-parser: 4.2.0
|
||||
dev: false
|
||||
|
||||
/csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
dev: false
|
||||
|
@ -1602,7 +1967,7 @@ packages:
|
|||
ms: 2.1.3
|
||||
dev: true
|
||||
|
||||
/debug@4.3.4:
|
||||
/debug@4.3.4(supports-color@5.5.0):
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
|
@ -1612,7 +1977,7 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
supports-color: 5.5.0
|
||||
|
||||
/decompress-response@6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
|
@ -1630,6 +1995,11 @@ packages:
|
|||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
dev: true
|
||||
|
||||
/deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/define-data-property@1.1.4:
|
||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -1678,6 +2048,10 @@ packages:
|
|||
esutils: 2.0.3
|
||||
dev: true
|
||||
|
||||
/electron-to-chromium@1.4.752:
|
||||
resolution: {integrity: sha512-P3QJreYI/AUTcfBVrC4zy9KvnZWekViThgQMX/VpJ+IsOBbcX5JFpORM4qWapwWQ+agb2nYAOyn/4PMXOk0m2Q==}
|
||||
dev: false
|
||||
|
||||
/emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
dev: true
|
||||
|
@ -1809,6 +2183,16 @@ packages:
|
|||
is-symbol: 1.0.4
|
||||
dev: true
|
||||
|
||||
/escalade@3.1.2:
|
||||
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/escape-string-regexp@1.0.5:
|
||||
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dev: false
|
||||
|
||||
/escape-string-regexp@4.0.0:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -1856,7 +2240,7 @@ packages:
|
|||
eslint: '*'
|
||||
eslint-plugin-import: '*'
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
enhanced-resolve: 5.16.0
|
||||
eslint: 8.57.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
|
@ -2027,7 +2411,7 @@ packages:
|
|||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.3
|
||||
debug: 4.3.4
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
doctrine: 3.0.0
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 7.2.2
|
||||
|
@ -2226,6 +2610,11 @@ packages:
|
|||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
dev: true
|
||||
|
||||
/gensync@1.0.0-beta.2:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: false
|
||||
|
||||
/get-intrinsic@1.2.4:
|
||||
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -2294,6 +2683,11 @@ packages:
|
|||
path-is-absolute: 1.0.1
|
||||
dev: true
|
||||
|
||||
/globals@11.12.0:
|
||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/globals@13.24.0:
|
||||
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -2346,6 +2740,10 @@ packages:
|
|||
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
|
||||
dev: true
|
||||
|
||||
/has-flag@3.0.0:
|
||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
/has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -2385,6 +2783,12 @@ packages:
|
|||
resolution: {integrity: sha512-Hnyf7ojTBtXHeOW1/t6wCBJSiK1WpoKF9yg7juxldDx8u3iswrkPt2wbOA/1NiwU4j27DSIVoIEJRAhcdMef/A==}
|
||||
dev: false
|
||||
|
||||
/hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
dev: false
|
||||
|
||||
/ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
dev: false
|
||||
|
@ -2658,6 +3062,12 @@ packages:
|
|||
argparse: 2.0.1
|
||||
dev: true
|
||||
|
||||
/jsesc@2.5.2:
|
||||
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
dev: true
|
||||
|
@ -2677,6 +3087,12 @@ packages:
|
|||
minimist: 1.2.8
|
||||
dev: true
|
||||
|
||||
/json5@2.2.3:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/jsx-ast-utils@3.3.5:
|
||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
@ -2742,6 +3158,12 @@ packages:
|
|||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
/lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
dev: false
|
||||
|
||||
/lru-cache@6.0.0:
|
||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -2810,7 +3232,6 @@ packages:
|
|||
|
||||
/ms@2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
dev: true
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
@ -2855,12 +3276,12 @@ packages:
|
|||
react: '>18.0.0'
|
||||
react-dom: '>18.0.0'
|
||||
dependencies:
|
||||
next: 14.0.4(react-dom@18.2.0)(react@18.2.0)
|
||||
next: 14.0.4(@babel/core@7.24.5)(react-dom@18.2.0)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/next@14.0.4(react-dom@18.2.0)(react@18.2.0):
|
||||
/next@14.0.4(@babel/core@7.24.5)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==}
|
||||
engines: {node: '>=18.17.0'}
|
||||
hasBin: true
|
||||
|
@ -2883,7 +3304,7 @@ packages:
|
|||
postcss: 8.4.31
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
styled-jsx: 5.1.1(react@18.2.0)
|
||||
styled-jsx: 5.1.1(@babel/core@7.24.5)(react@18.2.0)
|
||||
watchpack: 2.4.0
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 14.0.4
|
||||
|
@ -2908,7 +3329,7 @@ packages:
|
|||
react-dom: '>= 16.0.0'
|
||||
dependencies:
|
||||
'@types/nprogress': 0.2.3
|
||||
next: 14.0.4(react-dom@18.2.0)(react@18.2.0)
|
||||
next: 14.0.4(@babel/core@7.24.5)(react-dom@18.2.0)(react@18.2.0)
|
||||
nprogress: 0.2.0
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
|
@ -2926,6 +3347,10 @@ packages:
|
|||
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
|
||||
dev: false
|
||||
|
||||
/node-releases@2.0.14:
|
||||
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
|
||||
dev: false
|
||||
|
||||
/normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -3110,6 +3535,10 @@ packages:
|
|||
engines: {node: '>= 0.4'}
|
||||
dev: true
|
||||
|
||||
/postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
dev: false
|
||||
|
||||
/postcss@8.4.31:
|
||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
@ -3205,6 +3634,17 @@ packages:
|
|||
strip-json-comments: 2.0.1
|
||||
dev: false
|
||||
|
||||
/react-data-table-component@7.5.4(react@18.2.0)(styled-components@5.3.3):
|
||||
resolution: {integrity: sha512-6DGVj3urJZfEEMuP652fSjxdRVKeyb+9d0YounVc+MX8jwoyXQW6KO10eyZqElE9QtVrKrCeJxR7vht9yxyJiw==}
|
||||
peerDependencies:
|
||||
react: '>= 16.8.3'
|
||||
styled-components: '>= 4'
|
||||
dependencies:
|
||||
deepmerge: 4.3.1
|
||||
react: 18.2.0
|
||||
styled-components: 5.3.3(@babel/core@7.24.5)(react-dom@18.2.0)(react-is@16.13.1)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-dom@18.2.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
||||
peerDependencies:
|
||||
|
@ -3399,7 +3839,6 @@ packages:
|
|||
/semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/semver@7.6.0:
|
||||
resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
|
||||
|
@ -3429,6 +3868,10 @@ packages:
|
|||
has-property-descriptors: 1.0.2
|
||||
dev: true
|
||||
|
||||
/shallowequal@1.1.0:
|
||||
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
|
||||
dev: false
|
||||
|
||||
/sharp@0.30.7:
|
||||
resolution: {integrity: sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
|
@ -3615,7 +4058,32 @@ packages:
|
|||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/styled-jsx@5.1.1(react@18.2.0):
|
||||
/styled-components@5.3.3(@babel/core@7.24.5)(react-dom@18.2.0)(react-is@16.13.1)(react@18.2.0):
|
||||
resolution: {integrity: sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '>= 16.8.0'
|
||||
react-is: '>= 16.8.0'
|
||||
dependencies:
|
||||
'@babel/helper-module-imports': 7.24.3
|
||||
'@babel/traverse': 7.24.5(supports-color@5.5.0)
|
||||
'@emotion/is-prop-valid': 0.8.8
|
||||
'@emotion/stylis': 0.8.5
|
||||
'@emotion/unitless': 0.7.5
|
||||
babel-plugin-styled-components: 2.1.4(@babel/core@7.24.5)(styled-components@5.3.3)
|
||||
css-to-react-native: 3.2.0
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-is: 16.13.1
|
||||
shallowequal: 1.1.0
|
||||
supports-color: 5.5.0
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
dev: false
|
||||
|
||||
/styled-jsx@5.1.1(@babel/core@7.24.5)(react@18.2.0):
|
||||
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
peerDependencies:
|
||||
|
@ -3628,10 +4096,17 @@ packages:
|
|||
babel-plugin-macros:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/core': 7.24.5
|
||||
client-only: 0.0.1
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
has-flag: 3.0.0
|
||||
|
||||
/supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -3677,6 +4152,11 @@ packages:
|
|||
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
|
||||
dev: false
|
||||
|
||||
/to-fast-properties@2.0.0:
|
||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
@ -3800,6 +4280,17 @@ packages:
|
|||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
dev: true
|
||||
|
||||
/update-browserslist-db@1.0.13(browserslist@4.23.0):
|
||||
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
dependencies:
|
||||
browserslist: 4.23.0
|
||||
escalade: 3.1.2
|
||||
picocolors: 1.0.0
|
||||
dev: false
|
||||
|
||||
/uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
dependencies:
|
||||
|
@ -3893,6 +4384,10 @@ packages:
|
|||
sax: 1.3.0
|
||||
dev: false
|
||||
|
||||
/yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
dev: false
|
||||
|
||||
/yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
const http = require('node:http');
|
||||
const faye = require('faye');
|
||||
const url = require('url');
|
||||
const pkg = require('./package.json')
|
||||
require('dotenv').config()
|
||||
|
||||
var server = http.createServer(),
|
||||
bayeux = new faye.NodeAdapter({ mount: '/faye' });
|
||||
|
||||
|
||||
bayeux.on('subscribe', function (clientId, channel) {
|
||||
console.log(`bayeux client ${clientId} subscribed to ${channel}`)
|
||||
})
|
||||
|
||||
const client = bayeux.getClient()
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
function publish(msg) {
|
||||
client.publish('/signals', {
|
||||
text: msg
|
||||
})
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
publish('Hello mocha!')
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
server.on('request', (req, res) => {
|
||||
const parsedUrl = url.parse(req.url, true);
|
||||
if (parsedUrl.pathname === '/metrics') {
|
||||
// Handle the /metrics route
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end(`version=${pkg.version}`);
|
||||
}
|
||||
});
|
||||
|
||||
bayeux.attach(server);
|
||||
const port = process.env.PORT || 5000;
|
||||
console.log(`listening on ${port}`);
|
||||
server.listen(port);
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "futureporn-realtime",
|
||||
"version": "1.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
"faye": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.4.1",
|
||||
"mocha": "^10.4.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,652 @@
|
|||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.4.5
|
||||
faye:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
|
||||
devDependencies:
|
||||
chai:
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.1
|
||||
mocha:
|
||||
specifier: ^10.4.0
|
||||
version: 10.4.0
|
||||
|
||||
packages:
|
||||
|
||||
/ansi-colors@4.1.1:
|
||||
resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
dev: true
|
||||
|
||||
/anymatch@3.1.3:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
dependencies:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
dev: true
|
||||
|
||||
/asap@2.0.6:
|
||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||
dev: false
|
||||
|
||||
/assertion-error@1.1.0:
|
||||
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
|
||||
dev: true
|
||||
|
||||
/balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
dev: true
|
||||
|
||||
/binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/brace-expansion@2.0.1:
|
||||
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
dev: true
|
||||
|
||||
/braces@3.0.2:
|
||||
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
fill-range: 7.0.1
|
||||
dev: true
|
||||
|
||||
/browser-stdout@1.3.1:
|
||||
resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
|
||||
dev: true
|
||||
|
||||
/camelcase@6.3.0:
|
||||
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/chai@4.4.1:
|
||||
resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
assertion-error: 1.1.0
|
||||
check-error: 1.0.3
|
||||
deep-eql: 4.1.3
|
||||
get-func-name: 2.0.2
|
||||
loupe: 2.3.7
|
||||
pathval: 1.1.1
|
||||
type-detect: 4.0.8
|
||||
dev: true
|
||||
|
||||
/chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
dev: true
|
||||
|
||||
/check-error@1.0.3:
|
||||
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
dev: true
|
||||
|
||||
/chokidar@3.5.3:
|
||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
braces: 3.0.2
|
||||
glob-parent: 5.1.2
|
||||
is-binary-path: 2.1.0
|
||||
is-glob: 4.0.3
|
||||
normalize-path: 3.0.0
|
||||
readdirp: 3.6.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/cliui@7.0.4:
|
||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 7.0.0
|
||||
dev: true
|
||||
|
||||
/color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
dev: true
|
||||
|
||||
/color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
dev: true
|
||||
|
||||
/csprng@0.1.2:
|
||||
resolution: {integrity: sha512-D3WAbvvgUVIqSxUfdvLeGjuotsB32bvfVPd+AaaTWMtyUeC9zgCnw5xs94no89yFLVsafvY9dMZEhTwsY/ZecA==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
dependencies:
|
||||
sequin: 0.1.1
|
||||
dev: false
|
||||
|
||||
/debug@4.3.4(supports-color@8.1.1):
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
supports-color: 8.1.1
|
||||
dev: true
|
||||
|
||||
/decamelize@4.0.0:
|
||||
resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/deep-eql@4.1.3:
|
||||
resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
type-detect: 4.0.8
|
||||
dev: true
|
||||
|
||||
/diff@5.0.0:
|
||||
resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
dev: true
|
||||
|
||||
/dotenv@16.4.5:
|
||||
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
dev: true
|
||||
|
||||
/escalade@3.1.2:
|
||||
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/escape-string-regexp@4.0.0:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/faye-websocket@0.11.4:
|
||||
resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dependencies:
|
||||
websocket-driver: 0.7.4
|
||||
dev: false
|
||||
|
||||
/faye@1.4.0:
|
||||
resolution: {integrity: sha512-kRrIg4be8VNYhycS2PY//hpBJSzZPr/DBbcy9VWelhZMW3KhyLkQR0HL0k0MNpmVoNFF4EdfMFkNAWjTP65g6w==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dependencies:
|
||||
asap: 2.0.6
|
||||
csprng: 0.1.2
|
||||
faye-websocket: 0.11.4
|
||||
safe-buffer: 5.2.1
|
||||
tough-cookie: 4.1.4
|
||||
tunnel-agent: 0.6.0
|
||||
dev: false
|
||||
|
||||
/fill-range@7.0.1:
|
||||
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
dev: true
|
||||
|
||||
/find-up@5.0.0:
|
||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
locate-path: 6.0.0
|
||||
path-exists: 4.0.0
|
||||
dev: true
|
||||
|
||||
/flat@5.0.2:
|
||||
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
dev: true
|
||||
|
||||
/fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
dev: true
|
||||
|
||||
/get-func-name@2.0.2:
|
||||
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
|
||||
dev: true
|
||||
|
||||
/glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
dev: true
|
||||
|
||||
/glob@8.1.0:
|
||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
inflight: 1.0.6
|
||||
inherits: 2.0.4
|
||||
minimatch: 5.0.1
|
||||
once: 1.4.0
|
||||
dev: true
|
||||
|
||||
/has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/he@1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/http-parser-js@0.5.8:
|
||||
resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==}
|
||||
dev: false
|
||||
|
||||
/inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
wrappy: 1.0.2
|
||||
dev: true
|
||||
|
||||
/inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
dev: true
|
||||
|
||||
/is-binary-path@2.1.0:
|
||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
binary-extensions: 2.3.0
|
||||
dev: true
|
||||
|
||||
/is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-glob@4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
dev: true
|
||||
|
||||
/is-number@7.0.0:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
dev: true
|
||||
|
||||
/is-plain-obj@2.1.0:
|
||||
resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-unicode-supported@0.1.0:
|
||||
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/js-yaml@4.1.0:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
dev: true
|
||||
|
||||
/locate-path@6.0.0:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
dev: true
|
||||
|
||||
/log-symbols@4.1.0:
|
||||
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
is-unicode-supported: 0.1.0
|
||||
dev: true
|
||||
|
||||
/loupe@2.3.7:
|
||||
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
dev: true
|
||||
|
||||
/minimatch@5.0.1:
|
||||
resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
dev: true
|
||||
|
||||
/mocha@10.4.0:
|
||||
resolution: {integrity: sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==}
|
||||
engines: {node: '>= 14.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
ansi-colors: 4.1.1
|
||||
browser-stdout: 1.3.1
|
||||
chokidar: 3.5.3
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
diff: 5.0.0
|
||||
escape-string-regexp: 4.0.0
|
||||
find-up: 5.0.0
|
||||
glob: 8.1.0
|
||||
he: 1.2.0
|
||||
js-yaml: 4.1.0
|
||||
log-symbols: 4.1.0
|
||||
minimatch: 5.0.1
|
||||
ms: 2.1.3
|
||||
serialize-javascript: 6.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
supports-color: 8.1.1
|
||||
workerpool: 6.2.1
|
||||
yargs: 16.2.0
|
||||
yargs-parser: 20.2.4
|
||||
yargs-unparser: 2.0.0
|
||||
dev: true
|
||||
|
||||
/ms@2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
dev: true
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: true
|
||||
|
||||
/normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
dev: true
|
||||
|
||||
/p-limit@3.1.0:
|
||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
yocto-queue: 0.1.0
|
||||
dev: true
|
||||
|
||||
/p-locate@5.0.0:
|
||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
dev: true
|
||||
|
||||
/path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/pathval@1.1.1:
|
||||
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
|
||||
dev: true
|
||||
|
||||
/picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
dev: true
|
||||
|
||||
/psl@1.9.0:
|
||||
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
|
||||
dev: false
|
||||
|
||||
/punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/querystringify@2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
dev: false
|
||||
|
||||
/randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
dev: false
|
||||
|
||||
/safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
/sequin@0.1.1:
|
||||
resolution: {integrity: sha512-hJWMZRwP75ocoBM+1/YaCsvS0j5MTPeBHJkS2/wruehl9xwtX30HlDF1Gt6UZ8HHHY8SJa2/IL+jo+JJCd59rA==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
/serialize-javascript@6.0.0:
|
||||
resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==}
|
||||
dependencies:
|
||||
randombytes: 2.1.0
|
||||
dev: true
|
||||
|
||||
/string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
dev: true
|
||||
|
||||
/strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
dev: true
|
||||
|
||||
/strip-json-comments@3.1.1:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
dev: true
|
||||
|
||||
/supports-color@8.1.1:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
dev: true
|
||||
|
||||
/to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
dev: true
|
||||
|
||||
/tough-cookie@4.1.4:
|
||||
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
psl: 1.9.0
|
||||
punycode: 2.3.1
|
||||
universalify: 0.2.0
|
||||
url-parse: 1.5.10
|
||||
dev: false
|
||||
|
||||
/tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/type-detect@4.0.8:
|
||||
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/universalify@0.2.0:
|
||||
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
dev: false
|
||||
|
||||
/url-parse@1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
dev: false
|
||||
|
||||
/websocket-driver@0.7.4:
|
||||
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dependencies:
|
||||
http-parser-js: 0.5.8
|
||||
safe-buffer: 5.2.1
|
||||
websocket-extensions: 0.1.4
|
||||
dev: false
|
||||
|
||||
/websocket-extensions@0.1.4:
|
||||
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dev: false
|
||||
|
||||
/workerpool@6.2.1:
|
||||
resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==}
|
||||
dev: true
|
||||
|
||||
/wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
dev: true
|
||||
|
||||
/wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
dev: true
|
||||
|
||||
/y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/yargs-parser@20.2.4:
|
||||
resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/yargs-unparser@2.0.0:
|
||||
resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
camelcase: 6.3.0
|
||||
decamelize: 4.0.0
|
||||
flat: 5.0.2
|
||||
is-plain-obj: 2.1.0
|
||||
dev: true
|
||||
|
||||
/yargs@16.2.0:
|
||||
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
cliui: 7.0.4
|
||||
escalade: 3.1.2
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
string-width: 4.2.3
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 20.2.4
|
||||
dev: true
|
||||
|
||||
/yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
|
@ -0,0 +1,49 @@
|
|||
const expect = require('chai').expect
|
||||
const Faye = require('faye')
|
||||
|
||||
// for reference of _state contants,
|
||||
// @see https://github.com/faye/faye/blob/60141e8d3942a88ba3de6a9b3aef9503bc1bb1e6/src/protocol/client.js#L178
|
||||
const UNCONNECTED = 1
|
||||
const CONNECTING = 2
|
||||
const CONNECTED = 3
|
||||
const DISCONNECTED = 4
|
||||
|
||||
describe('faye', function () {
|
||||
describe('sending event', function () {
|
||||
let client = new Faye.Client('http://localhost:3535/faye');
|
||||
// let client = new Faye.Client('https://realtime.futureporn.net/faye');
|
||||
client.connect()
|
||||
it('should send a signal', function (done) {
|
||||
const pub = client.publish('/signals', {
|
||||
text: 'hello worldy 123!'
|
||||
})
|
||||
pub.callback(function () {
|
||||
expect(client._state).to.equal(CONNECTED)
|
||||
done()
|
||||
})
|
||||
pub.errback(function (e) {
|
||||
throw new Error(e)
|
||||
})
|
||||
})
|
||||
it('should receive a signal', function (done) {
|
||||
const sub = client.subscribe('/signals', function(message) {
|
||||
console.log('Got a message: ' + message.text);
|
||||
expect(message.text).to.equal('Hello mocha!')
|
||||
done()
|
||||
});
|
||||
sub.callback(function () {
|
||||
expect(client._state).to.equal(CONNECTED)
|
||||
})
|
||||
sub.errback(function (e) {
|
||||
throw new Error(e)
|
||||
})
|
||||
})
|
||||
this.afterEach(function () {
|
||||
expect(client._state).to.equal(CONNECTED)
|
||||
})
|
||||
this.afterAll(function () {
|
||||
client.disconnect()
|
||||
expect(client._state).to.equal(DISCONNECTED)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,9 @@
|
|||
.dockerignore
|
||||
.gitignore
|
||||
*~
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.git
|
||||
LICENSE
|
||||
.nvmrc
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extension": ["js"],
|
||||
"spec": "src/**/*.spec.js"
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# scout
|
||||
|
||||
Watches an e-mail inbox for going live notifications
|
||||
|
||||
Support for
|
||||
* [ ] Chaturbate
|
||||
* [ ] Fansly
|
||||
|
||||
## Design requirements
|
||||
|
||||
* [ ] get watched channels list from Strapi
|
||||
* [ ] every 3 mins, watch/unwatch based on channels list
|
||||
* [ ] watch important sources for go-live notifications
|
||||
* [ ] CB tab
|
||||
* [ ] email inbox
|
||||
* [ ] alerts realtime server when watched room goes live
|
||||
* [ ] logs chat messages
|
||||
* [ ] throws errors when unable to connect
|
||||
* [ ] runs browser headless
|
||||
* [ ] runs in the cloud
|
||||
* [ ] runs in k8s cluster
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "futureporn-scout",
|
||||
"type": "module",
|
||||
"version": "3.0.0",
|
||||
"description": "detect when a stream goes live",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "mocha",
|
||||
"start:email": "node ./src/index.email.js",
|
||||
"start:browser": "node ./src/index.browser.js",
|
||||
"start": "concurrently \"npm:start:email\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "@CJ_Clippy",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"concurrently": "^8.2.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"fastq": "^1.17.1",
|
||||
"faye": "^1.4.0",
|
||||
"imapflow": "^1.0.160",
|
||||
"limiter": "^2.0.1",
|
||||
"mailparser": "^3.7.1",
|
||||
"puppeteer": "^22.7.1",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"puppeteer-extra-plugin-repl": "^2.3.3",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||
"xpath": "^0.0.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^5.1.0",
|
||||
"mocha": "^10.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
*.txt
|
|
@ -0,0 +1,130 @@
|
|||
|
||||
import { ImapFlow } from 'imapflow';
|
||||
import EventEmitter from 'node:events';
|
||||
import 'dotenv/config';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import { RateLimiter } from 'limiter';
|
||||
|
||||
|
||||
|
||||
if (!process.env.SCOUT_IMAP_SERVER) throw new Error('SCOUT_IMAP_SERVER is missing from env');
|
||||
if (!process.env.SCOUT_IMAP_PORT) throw new Error('SCOUT_IMAP_PORT is missing from env');
|
||||
if (!process.env.SCOUT_IMAP_USERNAME) throw new Error('SCOUT_IMAP_USERNAME is missing from env');
|
||||
if (!process.env.SCOUT_IMAP_PASSWORD) throw new Error('SCOUT_IMAP_PASSWORD is missing from env');
|
||||
|
||||
const limiter = new RateLimiter({ tokensPerInterval: 1, interval: 3000 });
|
||||
|
||||
// https://stackoverflow.com/a/49428486/1004931
|
||||
function streamToString(stream) {
|
||||
const chunks = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
stream.on('error', (err) => reject(err));
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export class Email extends EventEmitter {
|
||||
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.client = null
|
||||
}
|
||||
|
||||
async archiveMessage(uid) {
|
||||
await limiter.removeTokens(1);
|
||||
await this.client.messageDelete(uid, { uid: true })
|
||||
}
|
||||
|
||||
async connect() {
|
||||
this.client = new ImapFlow({
|
||||
host: process.env.SCOUT_IMAP_SERVER,
|
||||
port: process.env.SCOUT_IMAP_PORT,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.SCOUT_IMAP_USERNAME,
|
||||
pass: process.env.SCOUT_IMAP_PASSWORD
|
||||
}
|
||||
});
|
||||
|
||||
this.registerEventListeners()
|
||||
await this.client.connect()
|
||||
const stat = await this.getStatus()
|
||||
if (stat.messages > 0) {
|
||||
await this.emitAllMessages()
|
||||
}
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
console.log(' RECONNECTING...')
|
||||
delete this.client
|
||||
await this.connect()
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
let lock = await this.client.getMailboxLock('INBOX');
|
||||
let status;
|
||||
try {
|
||||
status = await this.client.status('INBOX', { messages: true });
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
async loadMessage(uid) {
|
||||
console.log(` 💾 loading message uid=${uid}`)
|
||||
let lock = await this.client.getMailboxLock('INBOX');
|
||||
let dl, body
|
||||
try {
|
||||
dl = await this.client.download(uid, undefined, { uid: true })
|
||||
body = await streamToString(dl.content)
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
async emitAllMessages() {
|
||||
console.log('emitAllMessages is running')
|
||||
let lock = await this.client.getMailboxLock('INBOX');
|
||||
try {
|
||||
for await (let message of this.client.fetch('1:*', { envelope: true })) {
|
||||
// it is tempting to call this.client.download here, but that is not possible while the mailbox is locked.
|
||||
// client.download must be called outside of this lock
|
||||
// console.log('here is a message')
|
||||
console.log(JSON.stringify(message, null, 2))
|
||||
this.emit('message', message)
|
||||
}
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
registerEventListeners() {
|
||||
console.log(` > REGISTERING EVENT LISTENERS <`)
|
||||
this.client.once('end', () => this.reconnect())
|
||||
this.client.on('exists', (evt) => {
|
||||
// console.log(`exists event! count=${evt.count} prevCount=${evt.prevCount}`)
|
||||
// console.log(evt)
|
||||
if (evt.path === 'INBOX') {
|
||||
this.emitAllMessages()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // Select and lock a mailbox. Throws if mailbox does not exist
|
||||
// console.log('get lock')
|
||||
|
||||
|
||||
// // log out and close connection
|
||||
// // await client.logout();
|
|
@ -0,0 +1,174 @@
|
|||
// watches chaturbate for go live
|
||||
|
||||
import puppeteer from 'puppeteer-extra';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
|
||||
import delay from 'delay'
|
||||
import ReplPlugin from 'puppeteer-extra-plugin-repl'
|
||||
import fsp from 'fs/promises';
|
||||
import repl from 'puppeteer-extra-plugin-repl';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const browserDataDir = __dirname+'/futureporn-scout-datadir';
|
||||
|
||||
|
||||
const searchInputSelector = 'body > app-root > div > div.site-wrapper.nav-bar-visible.nav-bar-top-visible > div > app-explore-route > div > app-account-explore-route > div > input'
|
||||
const followButtonSelector = "xpath///app-account-follow-button/div/xd-localization-string[contains(., 'Follow')]"
|
||||
const onlineIndicator = 'a.online-indicator.is-live'
|
||||
|
||||
// @todo Get this value from Strapi
|
||||
const channels = [
|
||||
'avabrooks'
|
||||
]
|
||||
|
||||
|
||||
const scrollDown = async (page) => {
|
||||
console.log('scrolling down')
|
||||
await page.keyboard.press('PageDown')
|
||||
await page.evaluate(async () => {
|
||||
window.scrollBy(0, 500);
|
||||
});
|
||||
}
|
||||
|
||||
const handleOnlineChannel = async (page) => {
|
||||
const href = await page.evaluate(async () => {
|
||||
const onlineIndicator = 'a.online-indicator.is-live'
|
||||
const href = document.querySelector(onlineIndicator).href
|
||||
return href
|
||||
})
|
||||
console.log(`${href} is online!`)
|
||||
await fsp.appendFile('./data.csv', `${new Date().toISOString()},${href}\n`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open up a tab to the channel.
|
||||
* Intercept websockets events which tell us when the channel is online
|
||||
*/
|
||||
const monitor = async (browser, channel) => {
|
||||
if (!browser) throw new Error('monitor requires page arg');
|
||||
if (!channel) throw new Error('monitor requires channel arg');
|
||||
console.log(`monitoring ${channel}`)
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.setRequestInterception(true);
|
||||
|
||||
const url = await new Promise((resolve) => {
|
||||
page.on('request', interceptRequest => {
|
||||
const url = interceptRequest.url();
|
||||
console.log(url)
|
||||
if (url.startsWith('wss://realtime')) {
|
||||
console.log(`request! ${interceptRequest.url()} ${JSON.stringify(interceptRequest.headers(), null, 2)}`);
|
||||
resolve(interceptRequest.url());
|
||||
}
|
||||
interceptRequest.continue();
|
||||
});
|
||||
|
||||
page.goto(`https://chaturbate.com/${channel}`);
|
||||
})
|
||||
|
||||
await page.repl()
|
||||
}
|
||||
|
||||
const dash = async (page, channel) => {
|
||||
console.log('check')
|
||||
await page.goto(`https://chaturbate.com/${channel}`)
|
||||
|
||||
|
||||
// look for is-live indicator
|
||||
try {
|
||||
console.log('waiting for home page')
|
||||
await page.waitForSelector('div.stories-scroll-container', { timeout: 15000 })
|
||||
console.log('waiting for online indicators')
|
||||
await page.waitForSelector(onlineIndicator, { timeout: 10000 })
|
||||
console.log(`FOUND online channel! ${JSON.stringify(onlineIndicator, null, 2)}`);
|
||||
handleOnlineChannel(page)
|
||||
delay(1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.log('error on the dash. lets move on.')
|
||||
}
|
||||
}
|
||||
|
||||
const discover = async (page) => {
|
||||
|
||||
// Navigate the page to a URL
|
||||
await page.goto('https://fansly.com/explore/discover');
|
||||
|
||||
|
||||
console.log('wait for search input')
|
||||
await page.waitForSelector(searchInputSelector)
|
||||
|
||||
// console.log('click search input')
|
||||
// await page.click(searchInputSelector)
|
||||
|
||||
console.log('type a random letter')
|
||||
const letter = (() => {
|
||||
const letters = 'abcdefghijklmnopqrstuvwxyz'
|
||||
const randomIndex = Math.floor(Math.random() * letters.length);
|
||||
return letters[randomIndex];
|
||||
})()
|
||||
await page.type(searchInputSelector, letter)
|
||||
|
||||
|
||||
|
||||
// // Start an interactive REPL here with the `page` instance.
|
||||
// await page.repl()
|
||||
// // Afterwards start REPL with the `browser` instance.
|
||||
// await browser.repl()
|
||||
|
||||
let go = true
|
||||
setTimeout(() => {
|
||||
go = false
|
||||
}, 1000*30*1) // ~2 minutes of following
|
||||
|
||||
while (go) {
|
||||
try {
|
||||
console.log('Wait for Follow/Unfollow buttons')
|
||||
try {
|
||||
await page.waitForSelector(followButtonSelector, { timeout: 1000 })
|
||||
await page.click(followButtonSelector, { timeout: 1000 })
|
||||
} catch {}
|
||||
|
||||
// look for is-live indicator
|
||||
try {
|
||||
await page.waitForSelector(onlineIndicator, { timeout: 100 })
|
||||
console.log(`FOUND online channel! ${JSON.stringify(onlineIndicator, null, 2)}`);
|
||||
handleOnlineChannel(page)
|
||||
} catch (e) {
|
||||
// console.log('online channel not found')
|
||||
}
|
||||
|
||||
scrollDown(page)
|
||||
await page.keyboard.press('PageDown')
|
||||
} catch (e) {
|
||||
console.error('Error while waiting for follow button')
|
||||
console.error(e)
|
||||
}
|
||||
await delay(15000)
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// Launch the browser and open a new blank page
|
||||
puppeteer.use(StealthPlugin())
|
||||
puppeteer.use(ReplPlugin())
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: [
|
||||
`--user-data-dir=${browserDataDir}`
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
for (const ch of channels) {
|
||||
await monitor(browser, ch)
|
||||
// await dash(page)
|
||||
// await discover(page) // discover & follow
|
||||
// await dash(page) // dashboard & see who's live
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
})();
|
|
@ -0,0 +1,45 @@
|
|||
'use strict'
|
||||
|
||||
/**
|
||||
* watches an e-mail inbox for going live notifications
|
||||
*/
|
||||
|
||||
import { checkEmail } from './parsers.js'
|
||||
import { signalRealtime, createStreamInDb, pushToRecents } from './signals.js'
|
||||
import { Email } from './imap.js'
|
||||
import fastq from "fastq";
|
||||
|
||||
|
||||
const q = fastq.promise(handleMessage, 1)
|
||||
|
||||
async function handleMessage({email, msg}) {
|
||||
try {
|
||||
console.log(' ✏️ loading message')
|
||||
const body = await email.loadMessage(msg.uid)
|
||||
|
||||
console.log(' ✏️ checking e-mail')
|
||||
const { isMatch, url, platform, channel, displayName, date } = (await checkEmail(body))
|
||||
|
||||
console.log(' ✏️ signalling realtime')
|
||||
if (isMatch) await signalRealtime(url);
|
||||
|
||||
// console.log(' ✏️ creating stream entry in db')
|
||||
// if (isMatch) await createStreamInDb({ platform, channel, date });
|
||||
|
||||
console.log(' ✏️ pushToRecents')
|
||||
if (platform === 'Fansly') await pushToRecents({ date, url, channel, displayName });
|
||||
|
||||
console.log(' ✏️ archiving e-mail')
|
||||
await email.archiveMessage(msg.uid)
|
||||
} catch (e) {
|
||||
// console.error('error encoutered')
|
||||
console.error(` An error was encountered while handling the following e-mail message.\n${JSON.stringify(msg, null, 2)}\nError as follows.\n${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
(async () => {
|
||||
const email = new Email()
|
||||
email.on('message', (msg) => q.push({email, msg}))
|
||||
await email.connect()
|
||||
})()
|
|
@ -0,0 +1,76 @@
|
|||
|
||||
import { simpleParser } from 'mailparser';
|
||||
import { load } from 'cheerio'
|
||||
|
||||
const definitions = [
|
||||
{
|
||||
platform: 'Chaturbate',
|
||||
selectors: {
|
||||
channel: 'td[id*="onlinemessage"] a:nth-child(1)'
|
||||
},
|
||||
from: 'follownotify@bk.chaturbate.com',
|
||||
template: 'https://chaturbate.com/:channel'
|
||||
},
|
||||
{
|
||||
platform: 'Fansly',
|
||||
selectors: {
|
||||
url: ($) => $("a[href*='/live/']").attr('href'),
|
||||
displayName: 'div[class*="message-col"] div:nth-child(5)'
|
||||
},
|
||||
from: 'no-reply@fansly.com',
|
||||
template: 'https://fansly.com/live/:channel',
|
||||
regex: /https:\/\/fansly.com\/live\/([a-zA-Z0-9_]+)/
|
||||
}
|
||||
]
|
||||
|
||||
function render(template, values) {
|
||||
// console.log(`values=${values}`)
|
||||
// console.log(values)
|
||||
return template.replace(/:([a-zA-Z0-9_]+)/g, (match, key) => {
|
||||
// Replace :channel with the corresponding property from the values object
|
||||
return values[key] || match;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* checkEmail
|
||||
*
|
||||
* Check an e-mail for go-live notification content.
|
||||
*
|
||||
* @param {String} body -- raw mail body
|
||||
* @returns {Object} result
|
||||
* @returns {Boolean} result.isMatch true if e-mail contains a go-live notification
|
||||
* @returns {String} result.channel
|
||||
*/
|
||||
export async function checkEmail (body) {
|
||||
|
||||
const mail = await simpleParser(body)
|
||||
|
||||
|
||||
let res = {}
|
||||
let def = definitions.find((def) => def.from === mail.from.value[0].address)
|
||||
if (!def) return { isMatch: false, channel: null, platform: null, url: null };
|
||||
res.isMatch = true
|
||||
|
||||
// Step 0, get values from e-mail metadata
|
||||
res.platform = def.platform
|
||||
res.date = new Date(mail.date).toISOString()
|
||||
|
||||
// Step 1, get values using CSS selectors
|
||||
const $ = load(mail.html)
|
||||
for (const s in def.selectors) {
|
||||
res[s] = (def.selectors[s] instanceof Object) ? def.selectors[s]($) : $(def.selectors[s]).text()
|
||||
}
|
||||
|
||||
// Step 2, get values using regex & templates
|
||||
res.channel = (() => {
|
||||
if (res.channel) return res.channel;
|
||||
if (def.regex && res.url) return def.regex.exec(res.url).at(1);
|
||||
})()
|
||||
|
||||
res.url = res.url || render(def.template, {channel: res.channel})
|
||||
|
||||
|
||||
return res
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
'use strict'
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { checkEmail } from './parsers.js'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
describe('parsers', function () {
|
||||
describe('checkEmail', function () {
|
||||
it('should detect fansly e-mails', async function () {
|
||||
const mailBody = await fs.readFile(path.join(__dirname, './fixtures/fansly.fixture.txt'), { encoding: 'utf8' })
|
||||
const { isMatch, channel, platform, url, date } = await checkEmail(mailBody)
|
||||
expect(isMatch).to.equal(true, 'a Fansly heuristic was not found')
|
||||
expect(platform).to.equal('Fansly')
|
||||
expect(channel).to.equal('SkiaObsidian')
|
||||
expect(url).to.equal('https://fansly.com/live/SkiaObsidian')
|
||||
expect(date).to.equal('2024-05-05T03:04:33.000Z')
|
||||
})
|
||||
it('should detect cb e-mails', async function () {
|
||||
const mailBody = await fs.readFile(path.join(__dirname, './fixtures/chaturbate.fixture.txt'), { encoding: 'utf8' })
|
||||
const { isMatch, channel, platform, url, date } = await checkEmail(mailBody)
|
||||
expect(isMatch).to.equal(true, 'a CB heuristic was not found')
|
||||
expect(platform).to.equal('Chaturbate')
|
||||
expect(channel).to.equal('skyeanette')
|
||||
expect(url).to.equal('https://chaturbate.com/skyeanette')
|
||||
expect(date).to.equal('2023-07-24T01:08:28.000Z')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,67 @@
|
|||
|
||||
import 'dotenv/config'
|
||||
import Faye from 'faye'
|
||||
|
||||
if (!process.env.PUBSUB_SERVER_URL) throw new Error('PUBSUB_SERVER_URL is missing from env');
|
||||
if (!process.env.SCOUT_STRAPI_API_KEY) throw new Error('SCOUT_STRAPI_API_KEY is missing from env');
|
||||
if (!process.env.STRAPI_URL) throw new Error('STRAPI_URL is missing from env');
|
||||
if (!process.env.SCOUT_RECENTS_TOKEN) throw new Error('SCOUT_RECENTS_TOKEN is undefined in env');
|
||||
|
||||
|
||||
const faye = new Faye.Client(process.env.PUBSUB_SERVER_URL);
|
||||
|
||||
export async function signalRealtime (url) {
|
||||
faye.publish('/signals', {
|
||||
signal: 'startV2',
|
||||
url: url
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a database record which shows this stream exists
|
||||
*/
|
||||
export async function createStreamInDb ({ platform, channel, date }) {
|
||||
const res = await fetch(`${process.env.STRAPI_URL}/api/streams`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'authorization': `Bearer ${process.env.SCOUT_STRAPI_API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
isFanslyStream: (platform === 'Fansly') ? true : false,
|
||||
isChaturbateStream: (platform === 'Chaturbate') ? true : false,
|
||||
archiveStatus: 'missing',
|
||||
date: date
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* this function is for whoisonline.sbtp.xyz
|
||||
*
|
||||
* this is a short-term thing, feel free to delete when it's no longer useful
|
||||
*/
|
||||
export async function pushToRecents ({ url, channel, displayName, date }) {
|
||||
const lastSeen = new Date().toISOString()
|
||||
console.log(`pushToRecents with url=${url} channel=${channel} displayName=${displayName} date=${date}`)
|
||||
const res = await fetch('https://whoisonline.sbtp.xyz/recents', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'authorization': `Bearer ${process.env.SCOUT_RECENTS_TOKEN}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
url,
|
||||
channel,
|
||||
displayName,
|
||||
date
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
|
@ -80,6 +80,7 @@
|
|||
"strapi": {
|
||||
"uuid": false
|
||||
},
|
||||
"packageManager": "pnpm@8.10.5",
|
||||
"engines": {
|
||||
"node": "20.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
# Tiltfile for working with Next and Strapi locally
|
||||
|
||||
|
||||
# load('ext://cert_manager', 'deploy_cert_manager')
|
||||
# deploy_cert_manager()
|
||||
load('ext://dotenv', 'dotenv')
|
||||
dotenv(fn='.env')
|
||||
|
||||
|
||||
load('ext://helm_remote', 'helm_remote')
|
||||
helm_remote(
|
||||
'kubernetes-ingress-controller',
|
||||
repo_name='kubernetes-ingress-controller',
|
||||
repo_url='https://ngrok.github.io/kubernetes-ingress-controller',
|
||||
namespace='futureporn',
|
||||
create_namespace='false',
|
||||
set=[
|
||||
'credentials.apiKey=%s' % os.getenv('NGROK_API_KEY'),
|
||||
'credentials.authtoken=%s' % os.getenv('NGROK_AUTHTOKEN')
|
||||
]
|
||||
)
|
||||
|
||||
k8s_yaml(helm(
|
||||
'./charts/fp',
|
||||
values=['./charts/fp/values-dev.yaml'],
|
||||
))
|
||||
|
||||
|
||||
|
||||
# # docker_build('fp/link2cid', './packages/link2cid')
|
||||
# docker_build(
|
||||
# 'fp/strapi',
|
||||
# '.',
|
||||
# only=["./packages/strapi"],
|
||||
# dockerfile='d.strapi.dockerfile',
|
||||
# target='release',
|
||||
# live_update=[
|
||||
# sync('./packages/strapi', '/app')
|
||||
# ]
|
||||
# )
|
||||
|
||||
load('ext://uibutton', 'cmd_button')
|
||||
# @todo in the future we can add a button for seeding the db from scratch.
|
||||
# this would be good for onboarding devs
|
||||
# step 1 would be creating the db
|
||||
#
|
||||
|
||||
# step 2 is ???
|
||||
#
|
||||
# set -eu
|
||||
# # get k8s pod name from tilt resource name
|
||||
# POD_NAME="$(tilt get kubernetesdiscovery "$resource" -ojsonpath='{.status.pods[0].name}')"
|
||||
# kubectl exec "$POD_NAME" -- $command
|
||||
|
||||
|
||||
pg_seed_script = '''
|
||||
kubectl -n futureporn exec postgres -- psql -U postgres --command "CREATE DATABASE futureporn_db WITH OWNER = postgres ENCODING = 'UTF8' LOCALE_PROVIDER = 'libc' CONNECTION LIMIT = -1 IS_TEMPLATE = False;"
|
||||
'''
|
||||
cmd_button('postgres:seed',
|
||||
argv=['sh', '-c', pg_seed_script],
|
||||
resource='postgres',
|
||||
icon_name='dataset',
|
||||
text='seed db with sample data',
|
||||
)
|
||||
cmd_button('postgres:restore',
|
||||
argv=['sh', '-c', 'cd letters && yarn install'],
|
||||
resource='postgres',
|
||||
icon_name='cloud_download',
|
||||
text='restore db from backup',
|
||||
)
|
||||
pgadmin_import_script = '''
|
||||
# @todo this script is meant for first-time setup of the cluster
|
||||
# it sets up the UI to have a futureporn postgres server connection
|
||||
#
|
||||
# @see https://www.pgadmin.org/docs/pgadmin4/development/import_export_servers.html#json-format
|
||||
#
|
||||
|
||||
# futureporn.pgadmin.json
|
||||
{
|
||||
"Servers": {
|
||||
"1": {
|
||||
"Name": "futureporn",
|
||||
"Group": "Servers",
|
||||
"Host": "postgres.futureporn.svc.cluster.local",
|
||||
"Port": 5432,
|
||||
"MaintenanceDB": "postgres",
|
||||
"Username": "postgres",
|
||||
"UseSSHTunnel": 0,
|
||||
"TunnelPort": "22",
|
||||
"TunnelAuthentication": 0,
|
||||
"KerberosAuthentication": false,
|
||||
"ConnectionParameters": {
|
||||
"sslmode": "prefer",
|
||||
"connect_timeout": 10,
|
||||
"sslcert": "<STORAGE_DIR>/.postgresql/postgresql.crt",
|
||||
"sslkey": "<STORAGE_DIR>/.postgresql/postgresql.key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
kubectl -n futureporn exec pgadmin -- /usr/bin/python /pgadmin/setup.py load-servers futureporn.pgadmin.json
|
||||
'''
|
||||
cmd_button('pgadmin:server',
|
||||
argv=['sh', '-c', pgadmin_import_script],
|
||||
resource='pgadmin',
|
||||
icon_name='dataset',
|
||||
text='create connection'
|
||||
)
|
||||
|
||||
|
||||
## Uncomment the following for fp/next in dev mode
|
||||
## this is useful for changing the UI and seeing results
|
||||
docker_build(
|
||||
'fp/next',
|
||||
'.',
|
||||
only=['./pnpm-lock.yaml', './package.json', './packages/next'],
|
||||
dockerfile='d.next.dockerfile',
|
||||
target='dev',
|
||||
build_args={
|
||||
'NEXT_PUBLIC_STRAPI_URL': 'http://strapi.futureporn.svc.cluster.local:1337'
|
||||
},
|
||||
live_update=[
|
||||
sync('./packages/next', '/app')
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
docker_build(
|
||||
'fp/scout',
|
||||
'.',
|
||||
only=['./pnpm-lock.yaml', './package.json', './packages/scout'],
|
||||
dockerfile='d.scout.dockerfile',
|
||||
live_update=[
|
||||
sync('./packages/scout', '/app')
|
||||
]
|
||||
)
|
||||
k8s_resource(
|
||||
workload='kubernetes-ingress-controller-manager',
|
||||
links=[
|
||||
link(os.getenv('NGROK_URL'), 'Endpoint')
|
||||
],
|
||||
labels='ngrok'
|
||||
)
|
||||
|
||||
# k8s_resource(
|
||||
# workload='echo-deployment',
|
||||
# port_forwards=['8080'],
|
||||
# labels='debug'
|
||||
# )
|
||||
# k8s_resource(
|
||||
# workload='game-2048',
|
||||
# port_forwards=['8081:8080'],
|
||||
# labels='debug'
|
||||
# )
|
||||
|
||||
k8s_resource(
|
||||
workload='next',
|
||||
port_forwards=['3000']
|
||||
)
|
||||
k8s_resource(
|
||||
workload='strapi',
|
||||
port_forwards=['1337'],
|
||||
links=[
|
||||
link('http://localhost:1337/admin', 'Strapi Admin UI')
|
||||
]
|
||||
)
|
||||
k8s_resource(
|
||||
workload='postgres',
|
||||
)
|
||||
k8s_resource(
|
||||
workload='pgadmin',
|
||||
port_forwards=['5050']
|
||||
)
|
Loading…
Reference in New Issue