add /upload

This commit is contained in:
CJ_Clippy 2025-03-22 19:40:16 -08:00
parent 856070cca1
commit 055a3ab66f
72 changed files with 2984 additions and 350 deletions

@ -68,7 +68,7 @@ jobs:
POSTGRES_PASSWORD: ${{ secrets.DB_PASS }}
PGUSER: ${{ vars.DB_USER }}
ports:
- 5433:5432
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s

@ -1,11 +1,15 @@
@import "bulma";
@import "variables";
@import "@fortawesome/fontawesome-free/scss/brands";
@import "@fortawesome/fontawesome-free/scss/regular";
@import "@fortawesome/fontawesome-free/scss/solid";
@import "@fortawesome/fontawesome-free/scss/fontawesome";
// @import "@fortawesome/fontawesome-free/scss/brands";
// @import "@fortawesome/fontawesome-free/scss/regular";
// @import "@fortawesome/fontawesome-free/scss/solid";
// @import "@fortawesome/fontawesome-free/scss/fontawesome";
// @import "dropzone";
// @import "@uppy/core/dist/style.min.css";
// @import "@uppy/dashboard/dist/style.min.css";
.is-unclickable {

@ -0,0 +1,542 @@
/* dropzone.css */
@-webkit-keyframes passing-through {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px)
}
30%,
70% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px)
}
100% {
opacity: 0;
-webkit-transform: translateY(-40px);
-moz-transform: translateY(-40px);
-ms-transform: translateY(-40px);
-o-transform: translateY(-40px);
transform: translateY(-40px)
}
}
@-moz-keyframes passing-through {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px)
}
30%,
70% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px)
}
100% {
opacity: 0;
-webkit-transform: translateY(-40px);
-moz-transform: translateY(-40px);
-ms-transform: translateY(-40px);
-o-transform: translateY(-40px);
transform: translateY(-40px)
}
}
@keyframes passing-through {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px)
}
30%,
70% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px)
}
100% {
opacity: 0;
-webkit-transform: translateY(-40px);
-moz-transform: translateY(-40px);
-ms-transform: translateY(-40px);
-o-transform: translateY(-40px);
transform: translateY(-40px)
}
}
@-webkit-keyframes slide-in {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px)
}
30% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px)
}
}
@-moz-keyframes slide-in {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px)
}
30% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px)
}
}
@keyframes slide-in {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px)
}
30% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px)
}
}
@-webkit-keyframes pulse {
0% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1)
}
10% {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1)
}
20% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1)
}
}
@-moz-keyframes pulse {
0% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1)
}
10% {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1)
}
20% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1)
}
}
@keyframes pulse {
0% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1)
}
10% {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1)
}
20% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1)
}
}
.dropzone,
.dropzone * {
box-sizing: border-box
}
.dropzone {
min-height: 150px;
border: 2px solid rgba(0, 0, 0, .3);
background: rgb(24, 26, 27);
padding: 20px 20px
}
.dropzone.dz-clickable {
cursor: pointer
}
.dropzone.dz-clickable * {
cursor: default
}
.dropzone.dz-clickable .dz-message,
.dropzone.dz-clickable .dz-message * {
cursor: pointer
}
.dropzone.dz-started .dz-message {
display: none
}
.dropzone.dz-drag-hover {
border-style: solid
}
.dropzone.dz-drag-hover .dz-message {
opacity: .5
}
.dropzone .dz-message {
text-align: center;
margin: 2em 0
}
.dropzone .dz-message .dz-button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit
}
.dropzone .dz-preview {
position: relative;
display: inline-block;
vertical-align: top;
margin: 16px;
min-height: 100px
}
.dropzone .dz-preview:hover {
z-index: 1000
}
.dropzone .dz-preview:hover .dz-details {
opacity: 1
}
.dropzone .dz-preview.dz-file-preview .dz-image {
border-radius: 20px;
background: #999;
background: linear-gradient(to bottom, #eee, #ddd)
}
.dropzone .dz-preview.dz-file-preview .dz-details {
opacity: 1
}
.dropzone .dz-preview.dz-image-preview {
background: #fff
}
.dropzone .dz-preview.dz-image-preview .dz-details {
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
-ms-transition: opacity 0.2s linear;
-o-transition: opacity 0.2s linear;
transition: opacity 0.2s linear
}
.dropzone .dz-preview .dz-remove {
font-size: 14px;
text-align: center;
display: block;
cursor: pointer;
border: none
}
.dropzone .dz-preview .dz-remove:hover {
text-decoration: underline
}
.dropzone .dz-preview:hover .dz-details {
opacity: 1
}
.dropzone .dz-preview .dz-details {
z-index: 20;
position: absolute;
top: 0;
left: 0;
opacity: 0;
font-size: 13px;
min-width: 100%;
max-width: 100%;
padding: 2em 1em;
text-align: center;
color: rgba(0, 0, 0, .9);
line-height: 150%
}
.dropzone .dz-preview .dz-details .dz-size {
margin-bottom: 1em;
font-size: 16px
}
.dropzone .dz-preview .dz-details .dz-filename {
white-space: nowrap
}
.dropzone .dz-preview .dz-details .dz-filename:hover span {
border: 1px solid rgba(200, 200, 200, .8);
background-color: rgba(255, 255, 255, .8)
}
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) {
overflow: hidden;
text-overflow: ellipsis
}
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
border: 1px solid transparent
}
.dropzone .dz-preview .dz-details .dz-filename span,
.dropzone .dz-preview .dz-details .dz-size span {
background-color: rgba(255, 255, 255, .4);
padding: 0 .4em;
border-radius: 3px
}
.dropzone .dz-preview:hover .dz-image img {
-webkit-transform: scale(1.05, 1.05);
-moz-transform: scale(1.05, 1.05);
-ms-transform: scale(1.05, 1.05);
-o-transform: scale(1.05, 1.05);
transform: scale(1.05, 1.05);
-webkit-filter: blur(8px);
filter: blur(8px)
}
.dropzone .dz-preview .dz-image {
border-radius: 20px;
overflow: hidden;
width: 120px;
height: 120px;
position: relative;
display: block;
z-index: 10
}
.dropzone .dz-preview .dz-image img {
display: block
}
.dropzone .dz-preview.dz-success .dz-success-mark {
-webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
-moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
-ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
-o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)
}
.dropzone .dz-preview.dz-error .dz-error-mark {
opacity: 1;
-webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
-moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
-ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
-o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)
}
.dropzone .dz-preview .dz-success-mark,
.dropzone .dz-preview .dz-error-mark {
pointer-events: none;
opacity: 0;
z-index: 500;
position: absolute;
display: block;
top: 50%;
left: 50%;
margin-left: -27px;
margin-top: -27px
}
.dropzone .dz-preview .dz-success-mark svg,
.dropzone .dz-preview .dz-error-mark svg {
display: block;
width: 54px;
height: 54px
}
.dropzone .dz-preview.dz-processing .dz-progress {
opacity: 1;
-webkit-transition: all 0.2s linear;
-moz-transition: all 0.2s linear;
-ms-transition: all 0.2s linear;
-o-transition: all 0.2s linear;
transition: all 0.2s linear
}
.dropzone .dz-preview.dz-complete .dz-progress {
opacity: 0;
-webkit-transition: opacity 0.4s ease-in;
-moz-transition: opacity 0.4s ease-in;
-ms-transition: opacity 0.4s ease-in;
-o-transition: opacity 0.4s ease-in;
transition: opacity 0.4s ease-in
}
.dropzone .dz-preview:not(.dz-processing) .dz-progress {
-webkit-animation: pulse 6s ease infinite;
-moz-animation: pulse 6s ease infinite;
-ms-animation: pulse 6s ease infinite;
-o-animation: pulse 6s ease infinite;
animation: pulse 6s ease infinite
}
.dropzone .dz-preview .dz-progress {
opacity: 1;
z-index: 1000;
pointer-events: none;
position: absolute;
height: 16px;
left: 50%;
top: 50%;
margin-top: -8px;
width: 80px;
margin-left: -40px;
background: rgba(255, 255, 255, .9);
-webkit-transform: scale(1);
border-radius: 8px;
overflow: hidden
}
.dropzone .dz-preview .dz-progress .dz-upload {
background: #333;
background: linear-gradient(to bottom, #666, #444);
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 0;
-webkit-transition: width 300ms ease-in-out;
-moz-transition: width 300ms ease-in-out;
-ms-transition: width 300ms ease-in-out;
-o-transition: width 300ms ease-in-out;
transition: width 300ms ease-in-out
}
.dropzone .dz-preview.dz-error .dz-error-message {
display: block
}
.dropzone .dz-preview.dz-error:hover .dz-error-message {
opacity: 1;
pointer-events: auto
}
.dropzone .dz-preview .dz-error-message {
pointer-events: none;
z-index: 1000;
position: absolute;
display: block;
display: none;
opacity: 0;
-webkit-transition: opacity 0.3s ease;
-moz-transition: opacity 0.3s ease;
-ms-transition: opacity 0.3s ease;
-o-transition: opacity 0.3s ease;
transition: opacity 0.3s ease;
border-radius: 8px;
font-size: 13px;
top: 130px;
left: -10px;
width: 140px;
background: #be2626;
background: linear-gradient(to bottom, #be2626, #a92222);
padding: .5em 1.2em;
color: #454a4d #202324
}
.dropzone .dz-preview .dz-error-message:after {
content: "";
position: absolute;
top: -6px;
left: 64px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #be2626
}

@ -18,21 +18,24 @@
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import topbar from "../vendor/topbar"
import Hooks from './hooks/index.js'
import Uploaders from "./uploaders.js"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken},
hooks: Hooks
longPollFallbackMs: 3000,
params: { _csrf_token: csrfToken },
hooks: Hooks,
uploaders: Uploaders
})
// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

@ -1,7 +1,9 @@
import VideojsHook from "./videojs_hook.js"
import UppyHook from './uppy_hook.js'
let Hooks = {}
Hooks.VideojsHook = VideojsHook
Hooks.UppyHook = UppyHook
export default Hooks

@ -0,0 +1,139 @@
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import GoldenRetriever from '@uppy/golden-retriever'
import AwsS3 from '@uppy/aws-s3'
// @see https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/uppy/s3-multipart.js
const UppyHook = {
mounted() {
let live = this
const element = live.el;
// Get the value of the `data-endpoint` attribute
// const endpoint = element.getAttribute('data-endpoint');
new Uppy()
.use(Dashboard, {
inline: true,
target: '#uppy-dashboard',
theme: 'dark',
proudlyDisplayPoweredByUppy: false
})
.use(AwsS3, {
signPart(file, partData) {
return new Promise((resolve) => {
live.pushEvent('sign_part', partData, (reply, ref) => {
// console.log(`We got a response from sign_part pushEvent. reply=${JSON.stringify(reply)}`)
let { url, headers } = reply
// console.log(`got url=${url} and headers=${headers} from reply`)
resolve({
url,
headers
})
})
})
},
createMultipartUpload(file) {
// console.log(`createMultipartUpload with file ${JSON.stringify(file)}`)
let { name, type } = file.meta
let payload = { name, type }
return new Promise((resolve) => {
live.pushEvent('initiate_multipart_upload', payload, (reply, ref) => {
// console.log(`payload=${JSON.stringify(payload)}`)
// console.log(`initiate_multipart_upload pushEvent response callback.`)
// console.log(`got reply=${JSON.stringify(reply)}`)
// console.log(`got ref=${JSON.stringify(ref)}`)
let output = {
uploadId: reply?.upload_id,
key: reply?.key
}
resolve(output)
})
})
},
completeMultipartUpload(file, { key, uploadId, parts }) {
// console.log(`completeMultipartUpload with file ${JSON.stringify(file)}, parts=${JSON.stringify(parts)}`)
// parts = parts.map((part) => Object.assign({}, part, { etag: part.etag.replace(/^"|"$/, "") } ))
parts = parts.map((part) => ({
etag: part.ETag.replace(/"/g, ""),
part_number: part.PartNumber
}));
// console.log(parts)
let payload = { key, uploadId, parts }
return new Promise((resolve) => {
live.pushEvent('complete_multipart_upload', payload, (reply, ref) => {
// console.log(`payload=${JSON.stringify(payload)}`)
// console.log(`complete_multipart_upload pushEvent response callback.`)
// console.log(`got reply=${JSON.stringify(reply)}`)
// console.log(`got ref=${JSON.stringify(ref)}`)
let output = {
location
}
resolve(output)
})
})
},
getUploadParameters(file, options) {
// console.log(`getUploadParameters with file=${JSON.stringify(file)}, options=${JSON.stringify(options)}`)
return new Promise((resolve) => {
let payload = { type: file.type, name: file.name }
live.pushEvent('get_upload_parameters', payload, (reply, ref) => {
// console.log(`reply=${JSON.stringify(reply)}`)
let { type, method, url } = reply
let output = {
type, method, url
}
resolve(output)
})
})
},
listParts(file, { uploadId, key }) {
// console.log('pushing list_parts event')
let payload = {
uploadId,
key
}
live.pushEvent('list_parts', payload, (reply) => {
return reply
})
},
shouldUseMultipart(file) {
// Use multipart only for files larger than 100MiB.
// return file.size > 100 * 2 ** 20;
return file.size > 100 * 2;
},
});
},
beforeUpdate() { },
updated() {
// console.log("UppyHook updated");
},
destroyed() { },
disconnected() { },
reconnected() { }
}
export default UppyHook

@ -0,0 +1,26 @@
let Uploaders = {}
Uploaders.S3 = function (entries, onViewError) {
entries.forEach(entry => {
let xhr = new XMLHttpRequest()
onViewError(() => xhr.abort())
xhr.onload = () => xhr.status === 200 ? entry.progress(100) : entry.error()
xhr.onerror = () => entry.error()
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) {
let percent = Math.round((event.loaded / event.total) * 100)
if (percent < 100) { entry.progress(percent) }
}
})
let url = entry.meta.url
xhr.open("PUT", url, true)
xhr.send(entry.file)
})
}
export default Uploaders;

@ -6,6 +6,12 @@
"": {
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@mux/upchunk": "^3.5.0",
"@uppy/aws-s3": "^4.2.3",
"@uppy/core": "^4.4.3",
"@uppy/dashboard": "^4.3.2",
"@uppy/golden-retriever": "^4.1.1",
"dropzone": "^6.0.0-beta.2",
"hls.js": "^1.5.18",
"vidstack": "^1.12.12"
}
@ -44,12 +50,192 @@
"node": ">=6"
}
},
"node_modules/@mux/upchunk": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@mux/upchunk/-/upchunk-3.5.0.tgz",
"integrity": "sha512-D+TtvlujlZQjh5I+vFzJ31h5E1uVpEaLdR8M3BNaCFbVLnFMZs8J/L/fYSUyVGnyHT/yDtPHn/IHKdo3G6oSjA==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^6.0.2",
"xhr": "^2.6.0"
}
},
"node_modules/@swc/helpers": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.2.14.tgz",
"integrity": "sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA==",
"license": "MIT"
},
"node_modules/@transloadit/prettier-bytes": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.3.5.tgz",
"integrity": "sha512-xF4A3d/ZyX2LJWeQZREZQw+qFX4TGQ8bGVP97OLRt6sPO6T0TNHBFTuRHOJh7RNmYOBmQ9MHxpolD9bXihpuVA==",
"license": "MIT"
},
"node_modules/@types/retry": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@uppy/aws-s3": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@uppy/aws-s3/-/aws-s3-4.2.3.tgz",
"integrity": "sha512-5vNgTE85DLujOXpzC6KEwJHLSi8o96v4rwZxMvDWQuikvX4sGcGflYjBCsPaVDYUCiiDXuhI8f93zfwCUEwQ/Q==",
"license": "MIT",
"dependencies": {
"@uppy/companion-client": "^4.4.1",
"@uppy/utils": "^6.1.1"
},
"peerDependencies": {
"@uppy/core": "^4.4.1"
}
},
"node_modules/@uppy/companion-client": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-4.4.1.tgz",
"integrity": "sha512-ardMacShsfzaIbqHEH48YlpzWZkBj1qhAj0Dvn3r31p9d0HA5xFUvAdLYrZ6ezKvZ0RcDbf0SB5qCrQMkjscXQ==",
"license": "MIT",
"dependencies": {
"@uppy/utils": "^6.1.1",
"namespace-emitter": "^2.0.1",
"p-retry": "^6.1.0"
},
"peerDependencies": {
"@uppy/core": "^4.4.1"
}
},
"node_modules/@uppy/core": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/@uppy/core/-/core-4.4.3.tgz",
"integrity": "sha512-Ma/v9+u0xYoxFcTajBpe0TUHI0Vjw2IKgB0AUNevhgFsBRgA03nL5n8Fac8TrC0QjPkYu7h0n2xf2EgzvyxAQA==",
"license": "MIT",
"dependencies": {
"@transloadit/prettier-bytes": "^0.3.4",
"@uppy/store-default": "^4.2.0",
"@uppy/utils": "^6.1.2",
"lodash": "^4.17.21",
"mime-match": "^1.0.2",
"namespace-emitter": "^2.0.1",
"nanoid": "^5.0.9",
"preact": "^10.5.13"
}
},
"node_modules/@uppy/dashboard": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@uppy/dashboard/-/dashboard-4.3.2.tgz",
"integrity": "sha512-6cikgcY/TMy+Fq/v03QI1BNocfm1kOxii3kuUaxnz1SFGeuZ/55+C7KKL7SP/IdeoVwH7KV+550HThT0uwIQEw==",
"license": "MIT",
"dependencies": {
"@transloadit/prettier-bytes": "^0.3.4",
"@uppy/informer": "^4.2.1",
"@uppy/provider-views": "^4.4.2",
"@uppy/status-bar": "^4.1.2",
"@uppy/thumbnail-generator": "^4.1.1",
"@uppy/utils": "^6.1.2",
"classnames": "^2.2.6",
"lodash": "^4.17.21",
"memoize-one": "^6.0.0",
"nanoid": "^5.0.9",
"preact": "^10.5.13",
"shallow-equal": "^3.0.0"
},
"peerDependencies": {
"@uppy/core": "^4.4.2"
}
},
"node_modules/@uppy/golden-retriever": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@uppy/golden-retriever/-/golden-retriever-4.1.1.tgz",
"integrity": "sha512-ZzgG2p0iS/4xAOVQjckIOO29otZpxJEaZr6aDvNvc67eW0VhRMqWQhq/1X4bULmKg2TVIW06vaECd71DucUsQw==",
"license": "MIT",
"dependencies": {
"@uppy/utils": "^6.1.1",
"lodash": "^4.17.21"
},
"peerDependencies": {
"@uppy/core": "^4.4.1"
}
},
"node_modules/@uppy/informer": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@uppy/informer/-/informer-4.2.1.tgz",
"integrity": "sha512-0en8Py47pl6RMDrgUfqFoF807W5kK5AKVJNT1SkTsLiGg5anmTIMuvmNG3k6LN4cn9P/rKyEHSdGcoBBUj9u7Q==",
"license": "MIT",
"dependencies": {
"@uppy/utils": "^6.1.1",
"preact": "^10.5.13"
},
"peerDependencies": {
"@uppy/core": "^4.4.1"
}
},
"node_modules/@uppy/provider-views": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@uppy/provider-views/-/provider-views-4.4.2.tgz",
"integrity": "sha512-YGrPJuydrksmMCjvo7Ty7/lDLNo/Y8zsOgWgWmVbXB0V5aRvqY49LeKY8HDlOXclKmn6dl5CeQFf7p46txRNGQ==",
"license": "MIT",
"dependencies": {
"@uppy/utils": "^6.1.2",
"classnames": "^2.2.6",
"nanoid": "^5.0.9",
"p-queue": "^8.0.0",
"preact": "^10.5.13"
},
"peerDependencies": {
"@uppy/core": "^4.4.2"
}
},
"node_modules/@uppy/status-bar": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@uppy/status-bar/-/status-bar-4.1.2.tgz",
"integrity": "sha512-Z2fDXItoE940uMo3kwdDo4ZFPjTk5GY6y/C/G5+tSl6nL/IaDtWo5iVbAKHIH4s9SIRwCBgllEhxbmEPhuK7eA==",
"license": "MIT",
"dependencies": {
"@transloadit/prettier-bytes": "^0.3.4",
"@uppy/utils": "^6.1.2",
"classnames": "^2.2.6",
"preact": "^10.5.13"
},
"peerDependencies": {
"@uppy/core": "^4.4.2"
}
},
"node_modules/@uppy/store-default": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-4.2.0.tgz",
"integrity": "sha512-PieFVa8yTvRHIqsNKfpO/yaJw5Ae/hT7uT58ryw7gvCBY5bHrNWxH5N0XFe8PFHMpLpLn8v3UXGx9ib9QkB6+Q==",
"license": "MIT"
},
"node_modules/@uppy/thumbnail-generator": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@uppy/thumbnail-generator/-/thumbnail-generator-4.1.1.tgz",
"integrity": "sha512-65znkGNgVTbVte51IKOhgxOpHGSwYj9Qik2jF2ZBocMbhBY4gPkWFwqMrKQBfddA9KbUa4jVe1psxhAQTzYgiA==",
"license": "MIT",
"dependencies": {
"@uppy/utils": "^6.1.1",
"exifr": "^7.0.0"
},
"peerDependencies": {
"@uppy/core": "^4.4.1"
}
},
"node_modules/@uppy/utils": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-6.1.2.tgz",
"integrity": "sha512-PCrw6v51M6p3hlrlB2INmcocen4Dyjun1SobjVZRBkg4wutQE8ihZfSrH5ZE8UXFelufhtO16wlaZMi0EHk84w==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21",
"preact": "^10.5.13"
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@ -62,12 +248,91 @@
"node": ">=0.4.0"
}
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/dom-walk": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"node_modules/dropzone": {
"version": "6.0.0-beta.2",
"resolved": "https://registry.npmjs.org/dropzone/-/dropzone-6.0.0-beta.2.tgz",
"integrity": "sha512-k44yLuFFhRk53M8zP71FaaNzJYIzr99SKmpbO/oZKNslDjNXQsBTdfLs+iONd0U0L94zzlFzRnFdqbLcs7h9fQ==",
"license": "MIT",
"dependencies": {
"@swc/helpers": "^0.2.13",
"just-extend": "^5.0.0"
}
},
"node_modules/event-target-shim": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz",
"integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/exifr": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
"integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==",
"license": "MIT"
},
"node_modules/global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"license": "MIT",
"dependencies": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
},
"node_modules/hls.js": {
"version": "1.5.20",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.20.tgz",
"integrity": "sha512-uu0VXUK52JhihhnN/MVVo1lvqNNuhoxkonqgO3IpjvQiGpJBdIXMGkofjQb/j9zvV7a1SW8U9g1FslWx/1HOiQ==",
"license": "Apache-2.0"
},
"node_modules/is-function": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
"license": "MIT"
},
"node_modules/is-network-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
"integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/just-extend": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
"integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==",
"license": "MIT"
},
"node_modules/lit-html": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
@ -77,6 +342,12 @@
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/media-captions": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/media-captions/-/media-captions-1.0.4.tgz",
@ -86,6 +357,138 @@
"node": ">=16"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/mime-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz",
"integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
"license": "ISC",
"dependencies": {
"wildcard": "^1.1.0"
}
},
"node_modules/min-document": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
"dependencies": {
"dom-walk": "^0.1.0"
}
},
"node_modules/namespace-emitter": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
"integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/p-queue": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.0.tgz",
"integrity": "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^5.0.1",
"p-timeout": "^6.1.2"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-retry": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
"integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
"license": "MIT",
"dependencies": {
"@types/retry": "0.12.2",
"is-network-error": "^1.0.0",
"retry": "^0.13.1"
},
"engines": {
"node": ">=16.17"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-timeout": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz",
"integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse-headers": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz",
"integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==",
"license": "MIT"
},
"node_modules/preact": {
"version": "10.26.4",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.26.4.tgz",
"integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/shallow-equal": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz",
"integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==",
"license": "MIT"
},
"node_modules/unplugin": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
@ -119,6 +522,33 @@
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT"
},
"node_modules/wildcard": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz",
"integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
"license": "MIT"
},
"node_modules/xhr": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz",
"integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==",
"license": "MIT",
"dependencies": {
"global": "~4.4.0",
"is-function": "^1.0.1",
"parse-headers": "^2.0.0",
"xtend": "^4.0.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
}
}
}

@ -1,6 +1,12 @@
{
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@mux/upchunk": "^3.5.0",
"@uppy/aws-s3": "^4.2.3",
"@uppy/core": "^4.4.3",
"@uppy/dashboard": "^4.3.2",
"@uppy/golden-retriever": "^4.1.1",
"dropzone": "^6.0.0-beta.2",
"hls.js": "^1.5.18",
"vidstack": "^1.12.12"
}

@ -71,7 +71,7 @@ config :bright, Bright.Mailer, adapter: Swoosh.Adapters.Local
# Configure esbuild (the version is required)
config :esbuild,
version: "0.17.11",
version: "0.25.1",
bright: [
args:
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),

@ -5,12 +5,10 @@ defmodule Bright.ObanWorkers.ProcessPosts do
use Oban.Worker, queue: :default, max_attempts: 3
alias Bright.Vtubers.Vtuber
alias Bright.Repo
alias Bright.Socials.XPost
alias Bright.Streams.Stream
alias Bright.Platforms.Platform
alias Bright.Platforms
import Ecto.Query
require Logger
@ -26,28 +24,30 @@ defmodule Bright.ObanWorkers.ProcessPosts do
known_platforms = Platform |> Repo.all() |> Repo.preload(:platform_aliases)
{num, nil} =
XPost.get_unprocessed_posts()
|> then(fn posts ->
if posts == [] do
Logger.info("No unprocessed posts found")
else
Logger.debug("#{length(posts)} unprocessed posts found.")
Enum.each(posts, &process_post(&1, known_platforms))
mark_posts_as_processed(posts)
end
end)
posts = XPost.get_unprocessed_posts()
num = length(posts)
if posts == [] do
Logger.info("No unprocessed posts found")
else
Logger.debug("#{num} unprocessed posts found.")
Enum.each(posts, &process_post(&1, known_platforms))
mark_posts_as_processed(posts)
end
{:ok, num}
end
def process_post(post, known_platforms) do
with %{is_nsfw_live_announcement: is_live, platforms_mentioned: platforms} <-
XPost.parse(post, known_platforms),
{:ok, _stream} <- create_stream(post, platforms) do
:ok
else
idk ->
case XPost.parse(post, known_platforms) do
%{is_nsfw_live_announcement: true, platforms_mentioned: platforms_mentioned} ->
create_stream(post, platforms_mentioned)
# %{is_nsfw_live_announcement: false, platforms_mentioned: _} ->
# Logger.warning("is_nsfw_live_announcement was false for post with raw=#{post.raw}")
# :ok
_ ->
Logger.debug(
"process_post did not find a nsfw live announcement. post=#{inspect(post)} known_platforms=#{inspect(known_platforms)}"
)
@ -64,9 +64,7 @@ defmodule Bright.ObanWorkers.ProcessPosts do
date = post |> Map.get(:date)
title = "#{post.vtuber.display_name} #{date}"
Logger.debug(
"WE ARE CREATING A Stream with platforms=#{inspect(platforms)} title=#{inspect(title)} with date=#{inspect(date)} post=#{inspect(post)}"
)
Logger.warning("WE ARE CREATING A Stream with post.raw=#{inspect(post.raw)}")
changeset =
%Stream{}
@ -80,7 +78,7 @@ defmodule Bright.ObanWorkers.ProcessPosts do
Logger.info("Created stream: #{inspect(stream)}")
{:error, %Ecto.Changeset{errors: [date: {"has already been taken", _}]}} ->
Logger.warn("Stream #{title} already exists. Skipping...")
Logger.warning("Stream #{title} already exists. Skipping...")
{:error, changeset} ->
Logger.error(

@ -109,26 +109,6 @@ defmodule Bright.Platforms do
Platform.changeset(platform, attrs)
end
@doc """
Creates a platform alias.
## Examples
iex> create_platform_alias(%{field: value})
{:ok, %PlatformAlias{}}
iex> create_platform(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_platform_alias(attrs \\ %{}) do
IO.inspect(attrs, label: "PlatformAlias Attributes Before Insert")
%PlatformAlias{}
|> PlatformAlias.changeset(attrs)
|> Repo.insert()
end
def match_platform?(a, b) do
host_a = URI.parse(a.url).host
host_b = URI.parse(b.url).host
@ -147,4 +127,119 @@ defmodule Bright.Platforms do
Logger.debug("contains_platform? a=#{inspect(a)} b=#{inspect(b)}")
Enum.any?(a, fn plat -> Enum.any?(b, &match_platform?(plat, &1)) end)
end
alias Bright.Platforms.PlatformAlias
@doc """
Returns the list of platform_aliases.
## Examples
iex> list_platform_aliases()
[%PlatformAlias{}, ...]
"""
def list_platform_aliases do
PlatformAlias
|> Repo.all()
|> Repo.preload([:platform])
end
# def list_streams do
# Stream
# |> Repo.all()
# |> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post])
# end
@doc """
Gets a single platform_alias.
Raises `Ecto.NoResultsError` if the Platform alias does not exist.
## Examples
iex> get_platform_alias!(123)
%PlatformAlias{}
iex> get_platform_alias!(456)
** (Ecto.NoResultsError)
"""
def get_platform_alias!(id) do
PlatformAlias
|> Repo.get!(id)
|> Repo.preload([:platform])
end
# reference
# def get_stream!(id) do
# Stream
# |> Repo.get!(id)
# |> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post])
# end
@doc """
Creates a platform_alias.
## Examples
iex> create_platform_alias(%{field: value})
{:ok, %PlatformAlias{}}
iex> create_platform_alias(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_platform_alias(attrs \\ %{}) do
%PlatformAlias{}
|> PlatformAlias.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a platform_alias.
## Examples
iex> update_platform_alias(platform_alias, %{field: new_value})
{:ok, %PlatformAlias{}}
iex> update_platform_alias(platform_alias, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_platform_alias(%PlatformAlias{} = platform_alias, attrs) do
platform_alias
|> PlatformAlias.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a platform_alias.
## Examples
iex> delete_platform_alias(platform_alias)
{:ok, %PlatformAlias{}}
iex> delete_platform_alias(platform_alias)
{:error, %Ecto.Changeset{}}
"""
def delete_platform_alias(%PlatformAlias{} = platform_alias) do
Repo.delete(platform_alias)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking platform_alias changes.
## Examples
iex> change_platform_alias(platform_alias)
%Ecto.Changeset{data: %PlatformAlias{}}
"""
def change_platform_alias(%PlatformAlias{} = platform_alias, attrs \\ %{}) do
PlatformAlias.changeset(platform_alias, attrs)
end
end

@ -6,6 +6,7 @@ defmodule Bright.Socials.XPost do
alias Bright.Socials.{XPost, RSSParser}
alias Bright.Platforms.Platform
alias Bright.Platforms
alias Bright.Streams.Stream
alias Quinn
require Logger
@ -113,7 +114,7 @@ defmodule Bright.Socials.XPost do
"""
def includes_platform?(
raw_text,
%Platform{url: url, platform_aliases: %Ecto.Association.NotLoaded{}} = platform
%Platform{platform_aliases: %Ecto.Association.NotLoaded{}} = platform
) do
platform = Repo.preload(platform, :platform_aliases)
@ -178,7 +179,7 @@ defmodule Bright.Socials.XPost do
parse(post, known_platforms)
end
def parse(%XPost{vtuber: vtuber} = post, known_platforms) when is_list(known_platforms) do
def parse(%XPost{} = post, known_platforms) when is_list(known_platforms) do
is_nsfw_live_announcement = is_nsfw_live_announcement?(post, known_platforms)
platforms_mentioned = get_platforms_mentioned(post, known_platforms)
@ -195,7 +196,7 @@ defmodule Bright.Socials.XPost do
To be considered a NSFW live announcement, a post must satisfy all the following conditions.
* The post is authored by the lewdtuber
* The post does not contain, "VOD/i"
* The post does not contain, "VOD/i" or "/post"
* The post mentions a NSFW platform
* The post does not mention any SFW streaming platform.
@ -216,7 +217,7 @@ defmodule Bright.Socials.XPost do
conditions = [
{:is_not_rt, XPost.authored_by_vtuber?(post, vtuber)},
{:is_not_vod, not String.contains?(String.downcase(post.raw), "vod")},
{:is_not_vod, not String.contains?(String.downcase(post.raw), ["vod", "/post"])},
{:is_nsfw_platform, Platforms.contains_platform?(nsfw_platforms, mentioned_platforms)},
{:is_no_sfw_platform, not Platforms.contains_platform?(mentioned_platforms, sfw_platforms)}
]

@ -33,6 +33,7 @@ defmodule Bright.Streams do
"""
def list_streams do
Stream
|> order_by(desc: :date)
|> Repo.all()
|> Repo.preload([:tags, :vods, :vtubers, :platforms, :x_post])
end
@ -602,4 +603,100 @@ defmodule Bright.Streams do
Repo.aggregate(query, :count, :id)
end
alias Bright.Streams.Upload
@doc """
Returns the list of uploads.
## Examples
iex> list_uploads()
[%Upload{}, ...]
"""
def list_uploads do
Repo.all(Upload)
end
@doc """
Gets a single upload.
Raises `Ecto.NoResultsError` if the Upload does not exist.
## Examples
iex> get_upload!(123)
%Upload{}
iex> get_upload!(456)
** (Ecto.NoResultsError)
"""
def get_upload!(id), do: Repo.get!(Upload, id)
@doc """
Creates a upload.
## Examples
iex> create_upload(%{field: value})
{:ok, %Upload{}}
iex> create_upload(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_upload(attrs \\ %{}) do
%Upload{}
|> Upload.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a upload.
## Examples
iex> update_upload(upload, %{field: new_value})
{:ok, %Upload{}}
iex> update_upload(upload, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_upload(%Upload{} = upload, attrs) do
upload
|> Upload.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a upload.
## Examples
iex> delete_upload(upload)
{:ok, %Upload{}}
iex> delete_upload(upload)
{:error, %Ecto.Changeset{}}
"""
def delete_upload(%Upload{} = upload) do
Repo.delete(upload)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking upload changes.
## Examples
iex> change_upload(upload)
%Ecto.Changeset{data: %Upload{}}
"""
def change_upload(%Upload{} = upload, attrs \\ %{}) do
Upload.changeset(upload, attrs)
end
end

@ -0,0 +1,21 @@
defmodule Bright.Streams.Upload do
use Ecto.Schema
import Ecto.Changeset
schema "uploads" do
field :size, :integer
field :filename, :string
field :content_type, :string
field :user_id, :id
field :vod, :id
timestamps(type: :utc_datetime)
end
@doc false
def changeset(upload, attrs) do
upload
|> cast(attrs, [:filename, :size, :content_type])
|> validate_required([:filename, :size, :content_type])
end
end

@ -0,0 +1,17 @@
defmodule Bright.Utils do
@chars ~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
@doc """
Generates a random string of the given length.
## Parameters
- length: The length of the random string to generate.
## Examples
iex> RandomString.generate(12)
"aB3dEfG7hIjK"
"""
def random_string(length \\ 12) do
for _ <- 1..length, into: "", do: <<Enum.random(@chars)>>
end
end

@ -85,7 +85,6 @@ defmodule BrightWeb do
import Phoenix.HTML
# Core UI components and translation
import BrightWeb.CoreComponents
import BrightWeb.SVGIcon
import BrightWeb.Gettext
# Shortcut for generating JS commands

@ -18,7 +18,6 @@ defmodule BrightWeb.CoreComponents do
use Gettext, backend: BrightWeb.Gettext
alias Phoenix.LiveView.JS
import BrightWeb.SVGIcon
@doc """
Renders an external link.
@ -628,15 +627,11 @@ defmodule BrightWeb.CoreComponents do
# attr :style, :string, default: "solid"
# attr :class, :string, default: ""
# def icon(assigns) do
# ~H"""
# <i class={"fa#{style_prefix(@style)} fa-#{@name} #{@class}"}></i>
# """
# end
defp style_prefix("solid"), do: "s"
defp style_prefix("brands"), do: "b"
defp style_prefix(_), do: "s"
def icon(assigns) do
~H"""
<span>🇪</span>
"""
end
## JS Commands

@ -51,7 +51,9 @@
<div class="navbar-end">
<div class="navbar-item">
<.icon name="person_digging" type="solid" class="h-4 w-4 is-unclickable"/>
<.icon name="person_digging" type="solid" class="h-4 w-4 is-unclickable" />
<.icon name="magnet" type="solid" class="h-4 w-4 is-unclickable" />
<.icon name="circle-exclamation" type="solid" class="h-4 w-4 is-unclickable" />
<.icon name="hammer" class="is-unclickable" />
</div>

@ -1,122 +0,0 @@
defmodule BrightWeb.SVGIcon do
@moduledoc """
This package adds a convenient way of using svg icons with your Phoenix, Phoenix LiveView and Surface applications.
greets https://github.com/miguel-s/ex_heroicons/blob/main/lib/heroicons.ex
## Usage
<SVGIcons.icon name="chaturbate" class="h-4 w-4" />
## Config
Defaults can be set in the application configuration.
config :bright, :icons_type: "outline"
"""
use Phoenix.Component
alias BrightWeb.SVGIcon.Icon
svg_icons_path = "priv/static/assets/icons"
unless File.exists?(svg_icons_path) do
raise """
SVG icons not found. Expected to load them from #{svg_icons_path}.
"""
end
icon_paths =
svg_icons_path
|> Path.join("**/*.svg")
|> Path.wildcard()
icons =
for icon_path <- icon_paths do
@external_resource Path.relative_to_cwd(icon_path)
Icon.parse!(icon_path)
end
types = icons |> Enum.map(& &1.type) |> Enum.uniq()
names = icons |> Enum.map(& &1.name) |> Enum.uniq()
default_type =
case Application.compile_env(:bright, :icons_type) do
nil ->
"solid"
type when is_binary(type) ->
if type in types do
type
else
raise ArgumentError,
"expected default type to be one of #{inspect(types)}, got: #{inspect(type)}"
end
type ->
raise ArgumentError,
"expected default type to be one of #{inspect(types)}, got: #{inspect(type)}"
end
@names names
def names, do: @names
@types types
def types, do: @types
attr :name, :string, values: @names, required: true, doc: "the name of the icon"
attr :type, :string, values: @types, default: default_type, doc: "the type of the icon"
attr :class, :string, default: nil, doc: "the css classes to add to the svg container"
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the svg container"
def icon(assigns) do
name = assigns[:name]
if name == nil or name not in @names do
raise ArgumentError,
"expected icon name to be one of #{inspect(unquote(@names))}, got: #{inspect(name)}"
end
type = assigns[:type]
if type == nil or type not in @types do
raise ArgumentError,
"expected icon type to be one of #{inspect(unquote(@types))}, got: #{inspect(type)}"
end
~H"""
<span class="icon">
<.svg_container focusable="false" type={@type} class={@class} {@rest}>
<%= {:safe, svg_body(@name, @type)} %>
</.svg_container>
</span>
"""
end
attr :type, :string, values: @types, default: default_type, doc: "the type of the icon"
attr :class, :string, default: nil, doc: "the css classes to add to the svg container"
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the svg container"
slot :inner_block, required: true, doc: "the svg to render"
defp svg_container(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
defp svg_viewbox(type) do
case type do
"micro" -> "0 0 16 16"
"mini" -> "0 0 20 20"
"solid" -> "0 0 24 24"
"outline" -> "0 0 24 24"
end
end
for %Icon{name: name, type: type, file: file} <- icons do
defp svg_body(unquote(name), unquote(type)) do
unquote(file)
end
end
end

@ -1,44 +0,0 @@
defmodule BrightWeb.SVGIcon.Icon do
@moduledoc """
This module defines the data structure and functions for working with icons stored as SVG files.
"""
alias __MODULE__
@doc """
Defines the SVGIcon.Icon struct.
Its fields are:
* `:type` - the type of the icon
* `:name` - the name of the icon
* `:file` - the binary content of the file
"""
defstruct [:type, :name, :file]
@type t :: %Icon{type: String.t(), name: String.t(), file: binary}
@doc "Parses a SVG file and returns structured data"
@spec parse!(String.t()) :: Icon.t()
def parse!(filename) do
[type, name] =
filename
|> Path.split()
|> Enum.take(-2)
|> case do
["solid", name] -> ["solid", name]
["outline", name] -> ["outline", name]
end
name = Path.rootname(name)
file =
filename
|> File.read!()
|> String.split("\n")
|> Enum.map(&String.trim/1)
%__MODULE__{type: type, name: name, file: file}
end
end

@ -38,6 +38,10 @@ defmodule BrightWeb.PageController do
json(conn, data)
end
def upload(conn, _params) do
render(conn, :upload)
end
def redirect_test(conn, _params) do
render(conn, :home)
end

@ -8,8 +8,6 @@
</div>
</section>
<div class="section">
<p>
A platform built by fans, for fans, dedicated to preserving the moments that matter in the world of R-18 VTuber live streaming. It all started with a simple need: capturing ProjektMelody's streams on Chaturbate. Chaturbate doesnt save VODs, and sometimes we missed the magic. Other times, creators like ProjektMelody faced unnecessary de-platforming for simply being unique. We wanted to create a space where this content could endure, unshaken by the tides of censorship or fleeting platforms.

@ -9,15 +9,14 @@
<p>@todo documentation</p>
</section>
<div class="section">
<div class="section">
<p class="title">icons test</p>
<.icon name="bittorrent" type="solid"/>
<.icon name="hammer" type="solid"/>
<.icon name="fansly" type="solid"/>
<.icon name="reddit" type="solid"/>
<.icon name="chaturbate" type="solid"/>
<.icon name="throne" type="solid"/>
<.icon name="tiktok" type="solid"/>
<.icon name="pornhub" type="solid"/>
</div>
<.icon name="bittorrent" type="solid" />
<.icon name="hammer" type="solid" />
<.icon name="fansly" type="solid" />
<.icon name="reddit" type="solid" />
<.icon name="chaturbate" type="solid" />
<.icon name="throne" type="solid" />
<.icon name="tiktok" type="solid" />
<.icon name="pornhub" type="solid" />
</div>

@ -15,9 +15,7 @@
</p>
<i>
<p class="mt-3">
<% # <.icon name="circle-exclamation" class="icon h-4 w-4" /> %>
(Yeah, were still testing.) check back each week for updates.
<% # <.icon name="circle-exclamation" class="icon h-4 w-4" /> %> (Yeah, were still testing.) check back each week for updates.
</p>
</i>

@ -1,48 +0,0 @@
<.flash_group flash={@flash} />
<%= if @current_user do %>
<main class="section">
<h2 class="title is-2">Profile</h2>
<p class="subtitle">{@current_user.identicon_seed}</p>
</main>
<p class="title">@deprecated</p>
<section class="section">
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
{raw(
IdenticonSvg.generate(@current_user.identicon_seed, 5, :basic, 0.8, 2,
squircle_curvature: 0.8
)
)}
</figure>
</div>
<div class="media-content">
<p class="title is-4">{@current_user.identicon_seed}</p>
<p class="subtitle is-6">Github User {@current_user.github_id}</p>
</div>
</div>
<div class="content">
<p class="subtitle is-6">Futureporn User {@current_user.id}</p>
<p class="subtitle is-6"><i>n</i> uploads</p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nec
iaculis mauris. <.link>@bulmaio</.link>. <.link href="#">#css</.link>
<.link href="#">#responsive</.link>
<br />
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
</section>
<% else %>
<.flash_group flash={@flash} />
<section class="section">
<p>User not found in session.</p>
<p>Please <.link href={~p"/auth/patreon"}>sign in</.link></p>
</section>
<% end %>

@ -0,0 +1,62 @@
defmodule BrightWeb.PlatformAliasController do
use BrightWeb, :controller
alias Bright.Platforms
alias Bright.Platforms.PlatformAlias
def index(conn, _params) do
platform_aliases = Platforms.list_platform_aliases()
render(conn, :index, platform_aliases: platform_aliases)
end
def new(conn, _params) do
changeset = Platforms.change_platform_alias(%PlatformAlias{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"platform_alias" => platform_alias_params}) do
case Platforms.create_platform_alias(platform_alias_params) do
{:ok, platform_alias} ->
conn
|> put_flash(:info, "Platform alias created successfully.")
|> redirect(to: ~p"/platform_aliases/#{platform_alias}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
platform_alias = Platforms.get_platform_alias!(id)
render(conn, :show, platform_alias: platform_alias)
end
def edit(conn, %{"id" => id}) do
platform_alias = Platforms.get_platform_alias!(id)
changeset = Platforms.change_platform_alias(platform_alias)
render(conn, :edit, platform_alias: platform_alias, changeset: changeset)
end
def update(conn, %{"id" => id, "platform_alias" => platform_alias_params}) do
platform_alias = Platforms.get_platform_alias!(id)
case Platforms.update_platform_alias(platform_alias, platform_alias_params) do
{:ok, platform_alias} ->
conn
|> put_flash(:info, "Platform alias updated successfully.")
|> redirect(to: ~p"/platform_aliases/#{platform_alias}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, platform_alias: platform_alias, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
platform_alias = Platforms.get_platform_alias!(id)
{:ok, _platform_alias} = Platforms.delete_platform_alias(platform_alias)
conn
|> put_flash(:info, "Platform alias deleted successfully.")
|> redirect(to: ~p"/platform_aliases")
end
end

@ -0,0 +1,23 @@
defmodule BrightWeb.PlatformAliasHTML do
use BrightWeb, :html
embed_templates "platform_alias_html/*"
@doc """
Renders a platform_alias form.
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def platform_alias_form(assigns)
def platform_opts(changeset) do
existing_ids =
changeset
|> Ecto.Changeset.get_change(:vtubers, [])
|> Enum.map(& &1.data.id)
for platform <- Bright.Platforms.list_platforms(),
do: [key: platform.name, value: platform.id, selected: platform.id in existing_ids]
end
end

@ -0,0 +1,8 @@
<.header>
Edit Platform alias {@platform_alias.id}
<:subtitle>Use this form to manage platform_alias records in your database.</:subtitle>
</.header>
<.platform_alias_form changeset={@changeset} action={~p"/platform_aliases/#{@platform_alias}"} />
<.back navigate={~p"/platform_aliases"}>Back to platform_aliases</.back>

@ -0,0 +1,33 @@
<.header>
Listing Platform aliases
<:actions>
<.link href={~p"/platform_aliases/new"}>
<.button>New Platform alias</.button>
</.link>
</:actions>
</.header>
<.table
id="platform_aliases"
rows={@platform_aliases}
row_click={&JS.navigate(~p"/platform_aliases/#{&1}")}
>
<:col :let={platform_alias} label="Url">{platform_alias.url}</:col>
<:col :let={platform_alias} label="Platform">{platform_alias.platform.name}</:col>
<:action :let={platform_alias}>
<div class="sr-only">
<.link navigate={~p"/platform_aliases/#{platform_alias}"}>Show</.link>
</div>
<.link navigate={~p"/platform_aliases/#{platform_alias}/edit"}>Edit</.link>
</:action>
<:action :let={platform_alias}>
<.link
href={~p"/platform_aliases/#{platform_alias}"}
method="delete"
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>

@ -0,0 +1,8 @@
<.header>
New Platform alias
<:subtitle>Use this form to manage platform_alias records in your database.</:subtitle>
</.header>
<.platform_alias_form changeset={@changeset} action={~p"/platform_aliases"} />
<.back navigate={~p"/platform_aliases"}>Back to platform_aliases</.back>

@ -0,0 +1,16 @@
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:url]} type="text" label="Url" />
<.input
field={f[:platform_id]}
label="Platform"
type="select"
multiple={false}
options={platform_opts(@changeset)}
/>
<:actions>
<.button>Save Platform alias</.button>
</:actions>
</.simple_form>

@ -0,0 +1,16 @@
<.header>
Platform alias {@platform_alias.id}
<:subtitle>This is a platform_alias record from your database.</:subtitle>
<:actions>
<.link href={~p"/platform_aliases/#{@platform_alias}/edit"}>
<.button>Edit platform_alias</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Url">{@platform_alias.url}</:item>
<:item title="Platform">{@platform_alias.platform.name}</:item>
</.list>
<.back navigate={~p"/platform_aliases"}>Back to platform_aliases</.back>

@ -10,7 +10,7 @@
<.table id="platforms" rows={@platforms} row_click={&JS.navigate(~p"/platforms/#{&1}")}>
<:col :let={platform} label="Name">{platform.name}</:col>
<:col :let={platform} label="Url">{platform.url}</:col>
<:col :let={platform} label="Icon">{raw(platform.icon)}</:col>
<:col :let={platform} label="NSFW?">{platform.nsfw}</:col>
<:action :let={platform}>
<div class="sr-only">
<.link navigate={~p"/platforms/#{platform}"}>Show</.link>

@ -3,8 +3,9 @@
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:name]} type="text" label="Name" />
<.input field={f[:slug]} type="text" label="Slug" />
<.input field={f[:url]} type="text" label="Url" />
<.input field={f[:icon]} type="text" label="Icon" />
<.input field={f[:nsfw]} type="checkbox" label="NSFW?" />
<:actions>
<.button>Save Platform</.button>
</:actions>

@ -11,7 +11,7 @@
<.list>
<:item title="Name">{@platform.name}</:item>
<:item title="Url">{@platform.url}</:item>
<:item title="Icon">{raw(@platform.icon)}</:item>
<:item title="NSFW?">{@platform.nsfw}</:item>
</.list>
<.back navigate={~p"/platforms"}>Back to platforms</.back>

@ -8,15 +8,13 @@
</.header>
<.table id="streams" rows={@streams} row_click={&JS.navigate(~p"/streams/#{&1}")}>
<:col :let={stream} label="ID">{stream.id}</:col>
<:col :let={stream} label="Title">{stream.title}</:col>
<:col :let={stream} label="Date">{stream.date}</:col>
<:col :let={stream} label="Title">{stream.title}</:col>
<:col :let={stream} label="Platforms">
<div class="columns is-1">
<%= for platform <- stream.platforms do %>
<div class="column is-mobile is-narrow">
<%# {raw(platform.icon)} %>
<.icon name={platform.slug}/>
<.icon name={platform.slug} />
</div>
<% end %>
</div>

@ -0,0 +1,62 @@
defmodule BrightWeb.UploadController do
use BrightWeb, :controller
alias Bright.Streams
alias Bright.Streams.Upload
def index(conn, _params) do
uploads = Streams.list_uploads()
render(conn, :index, uploads: uploads)
end
def new(conn, _params) do
changeset = Streams.change_upload(%Upload{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"upload" => upload_params}) do
case Streams.create_upload(upload_params) do
{:ok, upload} ->
conn
|> put_flash(:info, "Upload created successfully.")
|> redirect(to: ~p"/uploads/#{upload}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
upload = Streams.get_upload!(id)
render(conn, :show, upload: upload)
end
def edit(conn, %{"id" => id}) do
upload = Streams.get_upload!(id)
changeset = Streams.change_upload(upload)
render(conn, :edit, upload: upload, changeset: changeset)
end
def update(conn, %{"id" => id, "upload" => upload_params}) do
upload = Streams.get_upload!(id)
case Streams.update_upload(upload, upload_params) do
{:ok, upload} ->
conn
|> put_flash(:info, "Upload updated successfully.")
|> redirect(to: ~p"/uploads/#{upload}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, upload: upload, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
upload = Streams.get_upload!(id)
{:ok, _upload} = Streams.delete_upload(upload)
conn
|> put_flash(:info, "Upload deleted successfully.")
|> redirect(to: ~p"/uploads")
end
end

@ -0,0 +1,13 @@
defmodule BrightWeb.UploadHTML do
use BrightWeb, :html
embed_templates "upload_html/*"
@doc """
Renders a upload form.
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def upload_form(assigns)
end

@ -0,0 +1,8 @@
<.header>
Edit Upload {@upload.id}
<:subtitle>Use this form to manage upload records in your database.</:subtitle>
</.header>
<.upload_form changeset={@changeset} action={~p"/uploads/#{@upload}"} />
<.back navigate={~p"/uploads"}>Back to uploads</.back>

@ -0,0 +1,25 @@
<.header>
Listing Uploads
<:actions>
<.link href={~p"/uploads/new"}>
<.button>New Upload</.button>
</.link>
</:actions>
</.header>
<.table id="uploads" rows={@uploads} row_click={&JS.navigate(~p"/uploads/#{&1}")}>
<:col :let={upload} label="Filename">{upload.filename}</:col>
<:col :let={upload} label="Size">{upload.size}</:col>
<:col :let={upload} label="Content type">{upload.content_type}</:col>
<:action :let={upload}>
<div class="sr-only">
<.link navigate={~p"/uploads/#{upload}"}>Show</.link>
</div>
<.link navigate={~p"/uploads/#{upload}/edit"}>Edit</.link>
</:action>
<:action :let={upload}>
<.link href={~p"/uploads/#{upload}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>

@ -0,0 +1,8 @@
<.header>
New Upload
<:subtitle>Use this form to manage upload records in your database.</:subtitle>
</.header>
<.upload_form changeset={@changeset} action={~p"/uploads"} />
<.back navigate={~p"/uploads"}>Back to uploads</.back>

@ -0,0 +1,17 @@
<.header>
Upload {@upload.id}
<:subtitle>This is a upload record from your database.</:subtitle>
<:actions>
<.link href={~p"/uploads/#{@upload}/edit"}>
<.button>Edit upload</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Filename">{@upload.filename}</:item>
<:item title="Size">{@upload.size}</:item>
<:item title="Content type">{@upload.content_type}</:item>
</.list>
<.back navigate={~p"/uploads"}>Back to uploads</.back>

@ -0,0 +1,11 @@
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:filename]} type="text" label="Filename" />
<.input field={f[:size]} type="number" label="Size" />
<.input field={f[:content_type]} type="text" label="Content type" />
<:actions>
<.button>Save Upload</.button>
</:actions>
</.simple_form>

@ -15,16 +15,19 @@
</figure>
</:col>
<:col :let={vtuber} label="Archival">
<% %{total_streams: total_streams, streams_with_vods: streams_with_vods, percentage: percentage} = Vtubers.vtuber_archive_percentage(vtuber) %>
<% %{
total_streams: total_streams,
streams_with_vods: streams_with_vods,
percentage: percentage
} = Vtubers.vtuber_archive_percentage(vtuber) %>
<div class="columns is-mobile">
<div class="column is-two-thirds">
<progress class="progress is-success" value={percentage} max="100">
<%= percentage %>%
{percentage}%
</progress>
</div>
<div class="column is-one-third">
<%= streams_with_vods %> of <%= total_streams %> (<%= percentage %>%)
{streams_with_vods} of {total_streams} ({percentage}%)
</div>
</div>
</:col>

@ -0,0 +1,84 @@
defmodule BrightWeb.UploadLive.FormComponent do
use BrightWeb, :live_component
alias Bright.Streams
@impl true
def render(assigns) do
~H"""
<div>
<.header>
{@title}
<:subtitle>Use this form to manage upload records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="upload-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:filename]} type="text" label="Filename" />
<.input field={@form[:size]} type="number" label="Size" />
<.input field={@form[:content_type]} type="text" label="Content type" />
<:actions>
<.button phx-disable-with="Saving...">Save Upload</.button>
</:actions>
</.simple_form>
</div>
"""
end
@impl true
def update(%{upload: upload} = assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_new(:form, fn ->
to_form(Streams.change_upload(upload))
end)}
end
@impl true
def handle_event("validate", %{"upload" => upload_params}, socket) do
changeset = Streams.change_upload(socket.assigns.upload, upload_params)
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
def handle_event("save", %{"upload" => upload_params}, socket) do
save_upload(socket, socket.assigns.action, upload_params)
end
defp save_upload(socket, :edit, upload_params) do
case Streams.update_upload(socket.assigns.upload, upload_params) do
{:ok, upload} ->
notify_parent({:saved, upload})
{:noreply,
socket
|> put_flash(:info, "Upload updated successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp save_upload(socket, :new, upload_params) do
case Streams.create_upload(upload_params) do
{:ok, upload} ->
notify_parent({:saved, upload})
{:noreply,
socket
|> put_flash(:info, "Upload created successfully")
|> push_patch(to: socket.assigns.patch)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

@ -0,0 +1,331 @@
defmodule BrightWeb.UploadLive.Index do
use BrightWeb, :live_view
alias Bright.Streams
alias Bright.Streams.Upload
alias Bright.Utils
require Logger
def mount(_params, _session, socket) do
# {:ok,
# socket
# |> assign(:uploaded_files, [])
# |> allow_upload(:vods,
# accept: ~w(.mp4 .mov .ts .avi .mpeg .ogv .webm .3gp .3g2),
# max_entries: 3,
# max_file_size: 80 * 1_000_000_000,
# external: &presign_upload/2
# )}
# socket = assign(socket, endpoint: System.fetch_env!("UPPY_ENDPOINT"))
socket =
socket
|> assign(:uploaded_files, [])
{:ok, socket}
end
@impl true
def render(assigns) do
~H"""
<div>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uppy/4.12.2/uppy.min.css" integrity="sha512-oPlr9/HXIlp7YoIRNsexheOu2/P2sEVi8EFQEAWUlHHijx0QbQ9qgihNYmIYtdJP3xOIMbZcnSVhrznIh5DKkg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<div id="uppy-dashboard" phx-hook="UppyHook" phx-update="ignore"></div>
</div>
"""
end
# @impl true
# def handle_event(
# "presign",
# %{"type" => type, "name" => name},
# socket
# ) do
# {:ok, %{url: url}, socket} = presign_url(name, type, socket)
# Logger.debug("✍️✍️✍️ presign called with name=#{name} type=#{type}. url=#{url}")
# # socket = assign(socket, signatures: {name, url})
# # socket =
# # socket
# # |> update(:uploaded_files, &(&1 ++ uploaded_files))
# # |> push_event("process_upload", %{name: name, url: url, type: type})
# {:reply, %{name: name, type: type, url: url}, socket}
# end
# @impl true
# def handle_event("upload_videos", _params, socket) do
# uploaded_files =
# consume_uploaded_entries(socket, :vods, fn %{path: _path}, entry ->
# IO.puts("@todo ⭐⭐⭐ Handling #{entry.client_type} @todo @todo @todo")
# IO.puts(inspect(entry))
# end)
# socket =
# socket
# |> update(:uploaded_files, &(&1 ++ uploaded_files))
# {:noreply, socket}
# end
# def handle_event("update_preview_srcs", %{"srcs" => srcs}, socket) do
# uploaded_files =
# socket.assigns.uploaded_files
# |> Enum.map(fn entry ->
# if Map.has_key?(srcs, entry.ref) do
# entry
# |> Map.put(:preview_src, Map.fetch!(srcs, entry.ref))
# else
# entry
# end
# end)
# socket =
# socket
# |> assign(:uploaded_files, uploaded_files)
# {:noreply, socket}
# end
# def handle_event("validate_upload", _params, socket) do
# num_remaining_uploads =
# length(socket.assigns.uploaded_files) - socket.assigns.uploads.vods.max_entries
# valid =
# Enum.uniq_by(socket.assigns.uploads.vods.entries, & &1.client_name)
# |> Enum.take(num_remaining_uploads)
# socket =
# Enum.reduce(socket.assigns.uploads.vods.entries, socket, fn entry, socket ->
# if entry in valid do
# socket
# else
# socket
# |> cancel_upload(:vods, entry.ref)
# |> put_flash(
# :error,
# "Uploaded files should be unique and cannot exceed #{socket.assigns.uploads.vods.max_entries} total files."
# )
# end
# end)
# {:noreply, socket}
# end
# def handle_event("cancel_upload", %{"ref" => ref}, socket) do
# {:noreply, cancel_upload(socket, :vods, ref)}
# end
# def handle_event("cancel_upload", _params, socket) do
# socket =
# Enum.reduce(socket.assigns.uploads.vods.entries, socket, fn entry, socket ->
# cancel_upload(socket, :vods, entry.ref)
# end)
# {:noreply, socket}
# end
# def presign_upload(name, type, socket) do
# Logger.debug("presign_upload was called with name=#{inspect(name)} and type=#{inspect(type)}")
# config = ExAws.Config.new(:s3)
# bucket = System.fetch_env!("AWS_BUCKET")
# key = "usc/#{Utils.random_string()}/#{name}"
# {:ok, url} =
# ExAws.S3.presigned_url(config, :put, bucket, key,
# expires_in: 3600,
# query_params: [{"Content-Type", type}]
# )
# {:ok, %{uploader: "S3", key: key, url: url}, socket}
# end
# # @doc @see https://hexdocs.pm/ex_aws_s3/ExAws.S3.html#presigned_post/4
# def presigned_post(name, socket) do
# Logger.debug("presigned_post with name=#{inspect(name)}")
# config = ExAws.Config.new(:s3)
# bucket = System.fetch_env!("AWS_BUCKET")
# key = "usc/#{Utils.random_string()}/#{name}"
# {:ok, url} = ExAws.S3.presigned_post(config, )
# end
defp get_s3_config(name) do
config = ExAws.Config.new(:s3)
bucket = System.fetch_env!("AWS_BUCKET")
key = "usc/#{Utils.random_string()}/#{name}"
%{config: config, bucket: bucket, key: key}
end
def handle_event(
"list_parts",
%{"upload_id" => upload_id, "key" => key},
socket
) do
config = ExAws.Config.new(:s3)
bucket = System.fetch_env!("AWS_BUCKET")
# key = "usc/#{Utils.random_string()}/#{name}"
# %{config: config, bucket: bucket, key: key} = get_s3_config(name)
case ExAws.S3.list_parts(bucket, key, upload_id) do
# <Part>
# <ETag>"85f30635602dc09bd85957a6e82a2c21"</ETag>
# <LastModified>2023-08-31T18:54:55.693Z</LastModified>
# <PartNumber>1</PartNumber>
# <Size>11</Size>
{:ok, part: %{etag: etag, partNumber: partNumber, size: size}} ->
Logger.debug("🦠🦠🦠 we got an etag from list_parts. etag=#{inspect(etag)}")
{:reply, %{etag: etag, partNumber: partNumber, size: size}, socket}
{:error, reason} ->
Logger.error("error while running S3.list_parts. reason=#{inspect(reason)}")
{:reply, %{error: "Failed to list_parts"}, socket}
end
end
# @doc @see https://hexdocs.pm/ex_aws_s3/ExAws.S3.html#initiate_multipart_upload/3
def handle_event(
"initiate_multipart_upload",
%{"name" => name, "type" => type},
socket
) do
%{config: config, bucket: bucket, key: key} = get_s3_config(name)
operation = ExAws.S3.initiate_multipart_upload(bucket, key, content_type: type)
case ExAws.request(operation, config) do
{:ok, %{body: %{key: key, upload_id: upload_id}} = response} ->
Logger.debug(
"Multipart upload initiated. Upload ID: #{upload_id}, Key: #{key}, response=#{inspect(response)}"
)
{:reply, %{upload_id: upload_id, key: key}, socket}
{:error, reason} ->
Logger.error("Failed to initiate multipart upload: #{inspect(reason)}")
{:reply, %{error: "Failed to initiate upload"}, socket}
end
end
def handle_event(
"abort_multipart_upload",
%{"key" => key, "uploadId" => upload_id},
socket
) do
config = ExAws.Config.new(:s3)
bucket = System.fetch_env!("AWS_BUCKET")
operation = ExAws.S3.abort_multipart_upload(bucket, key, upload_id)
case ExAws.request(operation, config) do
{:ok, _} ->
Logger.debug("Aborting multipart upload. Upload ID: #{upload_id}}")
{:reply, %{upload_id: upload_id}, socket}
{:error, reason} ->
Logger.error("Failed to abort multipart upload: #{inspect(reason)}")
{:reply, %{error: "Failed to initiate upload"}, socket}
end
end
def handle_event(
"get_upload_parameters",
%{"type" => type, "name" => name},
socket
) do
%{config: config, bucket: bucket, key: key} = get_s3_config(name)
{:ok, url} =
ExAws.S3.presigned_url(config, :put, bucket, key,
expires_in: 3600,
query_params: [
{"Content-Type", type}
]
)
{:reply, %{type: type, method: "PUT", url: url}, socket}
end
def handle_event(
"complete_multipart_upload",
%{"key" => key, "uploadId" => upload_id, "parts" => parts},
socket
) do
config = ExAws.Config.new(:s3)
bucket = System.fetch_env!("AWS_BUCKET")
Logger.debug("(before) parts=#{inspect(parts)}")
parts =
Enum.map(parts, fn part ->
{part["part_number"], part["etag"]}
end)
operation = ExAws.S3.complete_multipart_upload(bucket, key, upload_id, parts)
case ExAws.request(operation, config) do
{:ok, %{body: %{location: location}} = response} ->
Logger.debug(
"Completing multipart upload. Upload ID: #{upload_id}, response=#{inspect(response)}"
)
{:reply, %{location: location}, socket}
{:error, reason} ->
Logger.error("Failed to complete multipart upload: #{inspect(reason)}")
{:reply, %{error: "The server failed to complete multipart upload"}, socket}
end
end
def handle_event(
"sign_part",
%{"body" => _body, "key" => key, "partNumber" => part_number, "uploadId" => upload_id},
socket
) do
Logger.debug(
"sign_part was called with key=#{inspect(key)} part_number=#{inspect(part_number)} upload_id=#{inspect(upload_id)}"
)
config = ExAws.Config.new(:s3)
bucket = System.fetch_env!("AWS_BUCKET")
{:ok, url} =
ExAws.S3.presigned_url(config, :put, bucket, key,
expires_in: 3600,
query_params: [
{"partNumber", part_number},
{"uploadId", upload_id}
]
)
{:reply, %{url: url}, socket}
end
defp join_refs(entries), do: Enum.join(entries, ",")
def error_to_string(:too_large), do: "File too large!"
def error_to_string(:not_accepted), do: "Bad file type!"
defp to_megabytes_or_kilobytes(bytes) when is_integer(bytes) do
case bytes do
b when b < 1_048_576 ->
kilobytes = (b / 1024) |> Float.round(1)
if kilobytes < 1 do
"#{kilobytes}MB"
else
"#{round(kilobytes)}KB"
end
_ ->
megabytes = (bytes / 1_048_576) |> Float.round(1)
"#{megabytes}MB"
end
end
end

@ -0,0 +1,21 @@
defmodule BrightWeb.UploadLive.Show do
use BrightWeb, :live_view
alias Bright.Streams
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:upload, Streams.get_upload!(id))}
end
defp page_title(:show), do: "Show Upload"
defp page_title(:edit), do: "Edit Upload"
end

@ -0,0 +1,28 @@
<.header>
Upload {@upload.id}
<:subtitle>This is a upload record from your database.</:subtitle>
<:actions>
<.link patch={~p"/uploads/#{@upload}/show/edit"} phx-click={JS.push_focus()}>
<.button>Edit upload</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Filename">{@upload.filename}</:item>
<:item title="Size">{@upload.size}</:item>
<:item title="Content type">{@upload.content_type}</:item>
</.list>
<.back navigate={~p"/uploads"}>Back to uploads</.back>
<.modal :if={@live_action == :edit} id="upload-modal" show on_cancel={JS.patch(~p"/uploads/#{@upload}")}>
<.live_component
module={BrightWeb.UploadLive.FormComponent}
id={@upload.id}
title={@page_title}
action={@live_action}
upload={@upload}
patch={~p"/uploads/#{@upload}"}
/>
</.modal>

@ -0,0 +1,129 @@
defmodule SimpleS3Upload do
@moduledoc """
Dependency-free S3 Form Upload using HTTP POST sigv4
@see https://gist.github.com/chrismccord/37862f1f8b1f5148644b75d20d1cb073
https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
"""
@doc """
Signs a form upload.
The configuration is a map which must contain the following keys:
* `:region` - The AWS region, such as "us-east-1"
* `:access_key_id` - The AWS access key id
* `:secret_access_key` - The AWS secret access key
Returns a map of form fields to be used on the client via the JavaScript `FormData` API.
## Options
* `:key` - The required key of the object to be uploaded.
* `:max_file_size` - The required maximum allowed file size in bytes.
* `:content_type` - The required MIME type of the file to be uploaded.
* `:expires_in` - The required expiration time in milliseconds from now
before the signed upload expires.
## Examples
config = %{
region: "us-east-1",
access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"),
secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY")
}
{:ok, fields} =
SimpleS3Upload.sign_form_upload(config, "my-bucket",
key: "public/my-file-name",
content_type: "image/png",
max_file_size: 10_000,
expires_in: :timer.hours(1)
)
"""
def sign_form_upload(config, bucket, opts) do
key = Keyword.fetch!(opts, :key)
max_file_size = Keyword.fetch!(opts, :max_file_size)
content_type = Keyword.fetch!(opts, :content_type)
expires_in = Keyword.fetch!(opts, :expires_in)
expires_at = DateTime.add(DateTime.utc_now(), expires_in, :millisecond)
amz_date = amz_date(expires_at)
credential = credential(config, expires_at)
encoded_policy =
Base.encode64("""
{
"expiration": "#{DateTime.to_iso8601(expires_at)}",
"conditions": [
{"bucket": "#{bucket}"},
["eq", "$key", "#{key}"],
{"acl": "public-read"},
["eq", "$Content-Type", "#{content_type}"],
["content-length-range", 0, #{max_file_size}],
{"x-amz-server-side-encryption": "AES256"},
{"x-amz-credential": "#{credential}"},
{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
{"x-amz-date": "#{amz_date}"}
]
}
""")
fields = %{
"key" => key,
"acl" => "public-read",
"content-type" => content_type,
"x-amz-server-side-encryption" => "AES256",
"x-amz-credential" => credential,
"x-amz-algorithm" => "AWS4-HMAC-SHA256",
"x-amz-date" => amz_date,
"policy" => encoded_policy,
"x-amz-signature" => signature(config, expires_at, encoded_policy)
}
{:ok, fields}
end
defp amz_date(time) do
time
|> NaiveDateTime.to_iso8601()
|> String.split(".")
|> List.first()
|> String.replace("-", "")
|> String.replace(":", "")
|> Kernel.<>("Z")
end
defp credential(%{} = config, %DateTime{} = expires_at) do
"#{config.access_key_id}/#{short_date(expires_at)}/#{config.region}/s3/aws4_request"
end
defp signature(config, %DateTime{} = expires_at, encoded_policy) do
config
|> signing_key(expires_at, "s3")
|> sha256(encoded_policy)
|> Base.encode16(case: :lower)
end
defp signing_key(%{} = config, %DateTime{} = expires_at, service) when service in ["s3"] do
amz_date = short_date(expires_at)
%{secret_access_key: secret, region: region} = config
("AWS4" <> secret)
|> sha256(amz_date)
|> sha256(region)
|> sha256(service)
|> sha256("aws4_request")
end
defp short_date(%DateTime{} = expires_at) do
expires_at
|> amz_date()
|> String.slice(0..7)
end
defp sha256(secret, msg), do: :crypto.mac(:hmac, :sha256, secret, msg)
end

@ -0,0 +1,39 @@
# # lib/my_app_web/live/upload_live.ex
# defmodule MyAppWeb.UploadLive do
# use MyAppWeb, :live_view
# @impl Phoenix.LiveView
# def mount(_params, _session, socket) do
# {:ok,
# socket
# |> assign(:uploaded_files, [])
# |> allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 2)}
# end
# @impl Phoenix.LiveView
# def handle_event("validate", _params, socket) do
# {:noreply, socket}
# end
# @impl Phoenix.LiveView
# def handle_event("cancel-upload", %{"ref" => ref}, socket) do
# {:noreply, cancel_upload(socket, :avatar, ref)}
# end
# @impl Phoenix.LiveView
# def handle_event("save", _params, socket) do
# uploaded_files =
# consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
# dest = Path.join([:code.priv_dir(:my_app), "static", "uploads", Path.basename(path)])
# # You will need to create `priv/static/uploads` for `File.cp!/2` to work.
# File.cp!(path, dest)
# {:ok, ~p"/uploads/#{Path.basename(dest)}"}
# end)
# {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
# end
# defp error_to_string(:too_large), do: "Too large"
# defp error_to_string(:too_many_files), do: "You have selected too many files"
# defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
# end

@ -75,6 +75,7 @@ defmodule BrightWeb.Router do
# get "/vtubers/:id/edit", VtuberController, :edit
# end
resources("/uploads", UploadController, only: [:show, :index, :delete])
resources("/vods", VodController, only: [:create, :new, :edit, :update, :delete])
resources("/vtubers", VtuberController, only: [:delete])
@ -82,10 +83,12 @@ defmodule BrightWeb.Router do
resources("/torrents", TorrentController, only: [:new, :create, :edit, :update, :delete])
## !!! DANGER, platforms must only be writable by admins, (unless we implement SVG sanitizing)
## @todo remove SVGs from the database and instead put them in assets
resources("/platforms", PlatformController, only: [:new, :create, :edit, :update, :delete])
resources("/platform_aliases", PlatformAliasController,
only: [:new, :create, :edit, :update, :delete]
)
oban_dashboard("/oban")
end
@ -112,6 +115,9 @@ defmodule BrightWeb.Router do
get("/platforms", PlatformController, :index)
get("/platforms/:id", PlatformController, :show)
get("/platform_aliases", PlatformAliasController, :index)
get("/platform_aliases/:id", PlatformAliasController, :show)
resources("/vtubers", VtuberController, only: [:index, :show])
resources "/vt", VtuberController do
@ -122,6 +128,8 @@ defmodule BrightWeb.Router do
live_session :authenticated,
on_mount: [{BrightWeb.AuthController, :ensure_authenticated}] do
live("/profile", ProfileLive)
live("/upload", UploadLive.Index, :index)
# live("/upload/presign", , :)
end
end

@ -47,7 +47,7 @@ defmodule Bright.MixProject do
{:phoenix_live_view, "~> 1.0"},
{:floki, ">= 0.30.0", only: :test},
{:phoenix_live_dashboard, "~> 0.8.3"},
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
{:esbuild, "~> 0.9.0", runtime: Mix.env() == :dev},
{:dart_sass, "0.7.0", runtime: Mix.env() == :dev},
{:bulma, "1.0.2"},
{:swoosh, "~> 1.5"},
@ -74,7 +74,7 @@ defmodule Bright.MixProject do
{:bento, "~> 1.0"},
{:identicon_svg, "~> 0.9"},
{:excoveralls, "~> 0.18", only: :test},
{:quinn, "~> 1.1.3"},
{:quinn, "~> 1.1.3"}
]
end

@ -9,7 +9,7 @@
"bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"},
"bundlex": {:hex, :bundlex, "1.5.4", "3726acd463f4d31894a59bbc177c17f3b574634a524212f13469f41c4834a1d9", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "e745726606a560275182a8ac1c8ebd5e11a659bb7460d8abf30f397e59b4c5d2"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"},
"castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
"certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
"coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"},
@ -25,7 +25,7 @@
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
"esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"},
"esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
"ex_aws": {:hex, :ex_aws, "2.5.8", "0393cfbc5e4a9e7017845451a015d836a670397100aa4c86901980e2a2c5f7d4", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f79777b7932168956c8cc3a6db41f5783aa816eb50de356aed3165a71e5f8c3"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.6", "d135983bbd8b6df6350dfd83999437725527c1bea151e5055760bfc9b2d17c20", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "9874e12847e469ca2f13a5689be04e546c16f63caf6380870b7f25bf7cb98875"},
"ex_m3u8": {:hex, :ex_m3u8, "0.14.2", "3eb17f936e2ca2fdcde11664f3a543e75a94814d928098e050bda5b1e149c021", [:mix], [{:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "d2a1fb4382a521cce7f966502ecce6187f286ca2852dbb0dcc25dea72f8ba039"},
@ -109,7 +109,6 @@
"superstreamer_player": {:git, "https://github.com/superstreamerapp/superstreamer.git", "9e868acede851f396b3db98fb9799ab4bf712b02", [sparse: "packages/player"]},
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
"swoosh": {:hex, :swoosh, "1.17.6", "27ff070f96246e35b7105ab1c52b2b689f523a3cb83ed9faadb2f33bd653ccba", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9798f3e72165f40c950f6762c06dab68afcdcf616138fc4a07965c09c250e1e2"},
"tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},

@ -3,10 +3,14 @@ defmodule :"Elixir.Bright.Repo.Migrations.AddXPosts" do
def change do
create table(:x_posts) do
add :raw, :string, null: false # Raw content of the tweet
add :url, :string, null: false # URL of the tweet
add :date, :utc_datetime, null: false # Date and time of the tweet
add :is_invitation, :boolean, default: false # Whether the tweet contains an invite link
# Raw content of the tweet
add :raw, :string, null: false
# URL of the tweet
add :url, :string, null: false
# Date and time of the tweet
add :date, :utc_datetime, null: false
# Whether the tweet contains an invite link
add :is_invitation, :boolean, default: false
add :vtuber_id, references(:vtubers, on_delete: :delete_all), null: false
@ -19,5 +23,4 @@ defmodule :"Elixir.Bright.Repo.Migrations.AddXPosts" do
# Add an index on the `date` field for sorting and filtering
create index(:x_posts, [:date])
end
end

@ -0,0 +1,16 @@
defmodule Bright.Repo.Migrations.CreateUploads do
use Ecto.Migration
def change do
create table(:uploads) do
add :filename, :string
add :size, :integer
add :content_type, :string
add :user_id, references(:users, on_delete: :nothing)
timestamps(type: :utc_datetime)
end
create index(:uploads, [:user_id])
end
end

@ -0,0 +1,18 @@
defmodule Bright.Repo.Migrations.CreateUploads do
use Ecto.Migration
def change do
create table(:uploads) do
add :filename, :string
add :size, :integer
add :content_type, :string
add :user_id, references(:users, on_delete: :nothing)
add :vod, references(:vods, on_delete: :nothing)
timestamps(type: :utc_datetime)
end
create index(:uploads, [:user_id])
create index(:uploads, [:vod])
end
end

Binary file not shown.

After

(image error) Size: 323 B

@ -3,6 +3,7 @@ defmodule Bright.ImagesTest do
require Logger
alias Bright.Images
alias Bright.Utils
@test_mp4_fixture "./test/fixtures/SampleVideo_1280x720_1mb.mp4"
@test_ts_fixture "./test/fixtures/test-fixture.ts"
@ -24,13 +25,7 @@ defmodule Bright.ImagesTest do
basename = "thumb.jpg"
random_string =
for _ <- 1..12,
into: "",
do:
<<Enum.random(~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")>>
output_file = "/tmp/#{random_string}-#{basename}"
output_file = "/tmp/#{Utils.random_string()}-#{basename}"
Logger.debug(
"output_file=#{inspect(output_file)} @test_mp4_fixture=#{inspect(@test_mp4_fixture)}"

@ -56,7 +56,7 @@ defmodule Bright.ProcessPostsTest do
Socials.create_x_post(%{
raw: "I'm going live! fansly.com/fakename <3",
url: "https://x.com/fakename/status/283498235",
date: DateTime.utc_now(:second),
date: ~U[2025-03-18T13:25:39.000Z],
processed_at: nil,
vtuber_id: vtuber.id
}),
@ -64,7 +64,7 @@ defmodule Bright.ProcessPostsTest do
Socials.create_x_post(%{
raw: "gm! tiem for sex breakfast https://onlyfans.com/fakename",
url: "https://x.com/fakename/status/283498234",
date: DateTime.utc_now(:second),
date: ~U[2025-03-18T12:00:00.000Z],
processed_at: nil,
vtuber_id: vtuber.id
}),
@ -72,7 +72,7 @@ defmodule Bright.ProcessPostsTest do
Socials.create_x_post(%{
raw: "ero strim rn http://chaturbate.com/fakename",
url: "https://x.com/fakename/status/283498232",
date: DateTime.utc_now(:second),
date: ~U[2025-03-18T10:02:49.000Z],
processed_at: nil,
vtuber_id: vtuber.id
}),
@ -80,7 +80,7 @@ defmodule Bright.ProcessPostsTest do
Socials.create_x_post(%{
raw: "Join NOW for some fun! https://example.buzz",
url: "https://x.com/fakename/status/394848232",
date: DateTime.utc_now(:second),
date: ~U[2025-03-18T13:00:03.000Z],
processed_at: nil,
vtuber_id: vtuber.id
}),
@ -89,7 +89,7 @@ defmodule Bright.ProcessPostsTest do
Socials.create_x_post(%{
raw: "Let's play a game http://twitch.tv/fakename",
url: "https://x.com/fakename/status/283498343",
date: DateTime.utc_now(:second),
date: ~U[2025-03-18T03:19:23.000Z],
processed_at: nil,
vtuber_id: vtuber.id
}),
@ -98,7 +98,7 @@ defmodule Bright.ProcessPostsTest do
raw:
"Be sure to follow me on my socials http://chaturbate.com/fakename http://twitch.tv/fakename http://onlyfans.com/fakename http://linktree.com/fakename",
url: "https://x.com/fakename/status/283498349",
date: DateTime.utc_now(:second),
date: ~U[2025-03-18T14:31:30.000Z],
processed_at: nil,
vtuber_id: vtuber.id
})
@ -114,8 +114,7 @@ defmodule Bright.ProcessPostsTest do
test "create a stream for each post containing an invite link" do
{:ok, _} = perform_job(Bright.ObanWorkers.ProcessPosts, %{})
streams = Repo.all(Stream)
assert length(streams) == 4
assert length(streams) === 4
end
@tag :integration

@ -36,7 +36,11 @@ defmodule Bright.ObanWorkers.ProcessVodTest do
test "schedule ProcessVod when a new vod is created" do
stream = stream_fixture()
vod_fixture(%{thumbnail_url: nil, stream_id: stream.id, origin_temp_input_url: @example_url})
vod_fixture(%{
thumbnail_url: nil,
stream_id: stream.id,
origin_temp_input_url: @example_url
})
assert_enqueued(worker: ProcessVod, queue: :default)
end
@ -45,7 +49,11 @@ defmodule Bright.ObanWorkers.ProcessVodTest do
test "schedule CreateThumbnail when thumbnail_url is nil" do
stream = stream_fixture()
vod_fixture(%{thumbnail_url: nil, stream_id: stream.id, origin_temp_input_url: @example_url})
vod_fixture(%{
thumbnail_url: nil,
stream_id: stream.id,
origin_temp_input_url: @example_url
})
assert_enqueued(worker: ProcessVod, queue: :default)
# ProcessVod is what queues CreateThumbnail so we need to make it run

@ -95,4 +95,65 @@ defmodule Bright.PlatformsTest do
assert not Platforms.contains_platform?([], [platformA])
end
end
describe "platform_aliases" do
alias Bright.Platforms.PlatformAlias
import Bright.PlatformsFixtures
@invalid_attrs %{url: nil}
test "list_platform_aliases/0 returns all platform_aliases" do
platform_alias = platform_alias_fixture()
assert Platforms.list_platform_aliases() == [platform_alias]
end
test "get_platform_alias!/1 returns the platform_alias with given id" do
platform_alias = platform_alias_fixture()
assert Platforms.get_platform_alias!(platform_alias.id) == platform_alias
end
test "create_platform_alias/1 with valid data creates a platform_alias" do
valid_attrs = %{url: "some url"}
assert {:ok, %PlatformAlias{} = platform_alias} =
Platforms.create_platform_alias(valid_attrs)
assert platform_alias.url == "some url"
end
test "create_platform_alias/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Platforms.create_platform_alias(@invalid_attrs)
end
test "update_platform_alias/2 with valid data updates the platform_alias" do
platform_alias = platform_alias_fixture()
update_attrs = %{url: "some updated url"}
assert {:ok, %PlatformAlias{} = platform_alias} =
Platforms.update_platform_alias(platform_alias, update_attrs)
assert platform_alias.url == "some updated url"
end
test "update_platform_alias/2 with invalid data returns error changeset" do
platform_alias = platform_alias_fixture()
assert {:error, %Ecto.Changeset{}} =
Platforms.update_platform_alias(platform_alias, @invalid_attrs)
assert platform_alias == Platforms.get_platform_alias!(platform_alias.id)
end
test "delete_platform_alias/1 deletes the platform_alias" do
platform_alias = platform_alias_fixture()
assert {:ok, %PlatformAlias{}} = Platforms.delete_platform_alias(platform_alias)
assert_raise Ecto.NoResultsError, fn -> Platforms.get_platform_alias!(platform_alias.id) end
end
test "change_platform_alias/1 returns a platform_alias changeset" do
platform_alias = platform_alias_fixture()
assert %Ecto.Changeset{} = Platforms.change_platform_alias(platform_alias)
end
end
end

@ -449,6 +449,25 @@ defmodule Bright.XPostTest do
assert XPost.is_nsfw_live_announcement?(x_post, known_platforms)
end
test "should return false when receiving an XPost mentioning a Fansly /post", %{
known_platforms: known_platforms
} do
vtuber = VtubersFixtures.el_xox_fixture()
x_post =
%XPost{
url: "https://x.com/el_XoX34/status/1899913287808262522",
raw:
"Being a good girl and taking all of your big cock inside me 🍆💦 Subscribe Tier 2 Cummy Bunny 🐰💙 or pay $15 to see 5 spicy photos!! ▶️ fansly.com/post/754877662",
date: ~U[2025-03-12T20:00:01Z],
processed_at: nil,
vtuber_id: vtuber.id
}
|> Repo.preload(:vtuber)
assert not XPost.is_nsfw_live_announcement?(x_post, known_platforms)
end
test "should return false when the XPost is a retweet", %{
known_platforms: known_platforms
} do

@ -188,4 +188,62 @@ defmodule Bright.StreamsTest do
# assert_received {:progress, %{stage: :generating_thumbnail, done: 1, total: 1}}
end
end
describe "uploads" do
alias Bright.Streams.Upload
import Bright.StreamsFixtures
@invalid_attrs %{size: nil, filename: nil, content_type: nil}
test "list_uploads/0 returns all uploads" do
upload = upload_fixture()
assert Streams.list_uploads() == [upload]
end
test "get_upload!/1 returns the upload with given id" do
upload = upload_fixture()
assert Streams.get_upload!(upload.id) == upload
end
test "create_upload/1 with valid data creates a upload" do
valid_attrs = %{size: 42, filename: "some filename", content_type: "some content_type"}
assert {:ok, %Upload{} = upload} = Streams.create_upload(valid_attrs)
assert upload.size == 42
assert upload.filename == "some filename"
assert upload.content_type == "some content_type"
end
test "create_upload/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Streams.create_upload(@invalid_attrs)
end
test "update_upload/2 with valid data updates the upload" do
upload = upload_fixture()
update_attrs = %{size: 43, filename: "some updated filename", content_type: "some updated content_type"}
assert {:ok, %Upload{} = upload} = Streams.update_upload(upload, update_attrs)
assert upload.size == 43
assert upload.filename == "some updated filename"
assert upload.content_type == "some updated content_type"
end
test "update_upload/2 with invalid data returns error changeset" do
upload = upload_fixture()
assert {:error, %Ecto.Changeset{}} = Streams.update_upload(upload, @invalid_attrs)
assert upload == Streams.get_upload!(upload.id)
end
test "delete_upload/1 deletes the upload" do
upload = upload_fixture()
assert {:ok, %Upload{}} = Streams.delete_upload(upload)
assert_raise Ecto.NoResultsError, fn -> Streams.get_upload!(upload.id) end
end
test "change_upload/1 returns a upload changeset" do
upload = upload_fixture()
assert %Ecto.Changeset{} = Streams.change_upload(upload)
end
end
end

@ -0,0 +1,87 @@
defmodule BrightWeb.PlatformAliasControllerTest do
use BrightWeb.ConnCase
import Bright.PlatformsFixtures
@create_attrs %{url: "some url"}
@update_attrs %{url: "some updated url"}
@invalid_attrs %{url: nil}
describe "index" do
test "lists all platform_aliases", %{conn: conn} do
conn = get(conn, ~p"/platform_aliases")
assert html_response(conn, 200) =~ "Listing Platform aliases"
end
end
describe "new platform_alias" do
test "renders form", %{conn: conn} do
conn = get(conn, ~p"/platform_aliases/new")
assert html_response(conn, 200) =~ "New Platform alias"
end
end
describe "create platform_alias" do
test "redirects to show when data is valid", %{conn: conn} do
conn = post(conn, ~p"/platform_aliases", platform_alias: @create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == ~p"/platform_aliases/#{id}"
conn = get(conn, ~p"/platform_aliases/#{id}")
assert html_response(conn, 200) =~ "Platform alias #{id}"
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, ~p"/platform_aliases", platform_alias: @invalid_attrs)
assert html_response(conn, 200) =~ "New Platform alias"
end
end
describe "edit platform_alias" do
setup [:create_platform_alias]
test "renders form for editing chosen platform_alias", %{
conn: conn,
platform_alias: platform_alias
} do
conn = get(conn, ~p"/platform_aliases/#{platform_alias}/edit")
assert html_response(conn, 200) =~ "Edit Platform alias"
end
end
describe "update platform_alias" do
setup [:create_platform_alias]
test "redirects when data is valid", %{conn: conn, platform_alias: platform_alias} do
conn = put(conn, ~p"/platform_aliases/#{platform_alias}", platform_alias: @update_attrs)
assert redirected_to(conn) == ~p"/platform_aliases/#{platform_alias}"
conn = get(conn, ~p"/platform_aliases/#{platform_alias}")
assert html_response(conn, 200) =~ "some updated url"
end
test "renders errors when data is invalid", %{conn: conn, platform_alias: platform_alias} do
conn = put(conn, ~p"/platform_aliases/#{platform_alias}", platform_alias: @invalid_attrs)
assert html_response(conn, 200) =~ "Edit Platform alias"
end
end
describe "delete platform_alias" do
setup [:create_platform_alias]
test "deletes chosen platform_alias", %{conn: conn, platform_alias: platform_alias} do
conn = delete(conn, ~p"/platform_aliases/#{platform_alias}")
assert redirected_to(conn) == ~p"/platform_aliases"
assert_error_sent 404, fn ->
get(conn, ~p"/platform_aliases/#{platform_alias}")
end
end
end
defp create_platform_alias(_) do
platform_alias = platform_alias_fixture()
%{platform_alias: platform_alias}
end
end

@ -0,0 +1,84 @@
defmodule BrightWeb.UploadControllerTest do
use BrightWeb.ConnCase
import Bright.StreamsFixtures
@create_attrs %{size: 42, filename: "some filename", content_type: "some content_type"}
@update_attrs %{size: 43, filename: "some updated filename", content_type: "some updated content_type"}
@invalid_attrs %{size: nil, filename: nil, content_type: nil}
describe "index" do
test "lists all uploads", %{conn: conn} do
conn = get(conn, ~p"/uploads")
assert html_response(conn, 200) =~ "Listing Uploads"
end
end
describe "new upload" do
test "renders form", %{conn: conn} do
conn = get(conn, ~p"/uploads/new")
assert html_response(conn, 200) =~ "New Upload"
end
end
describe "create upload" do
test "redirects to show when data is valid", %{conn: conn} do
conn = post(conn, ~p"/uploads", upload: @create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == ~p"/uploads/#{id}"
conn = get(conn, ~p"/uploads/#{id}")
assert html_response(conn, 200) =~ "Upload #{id}"
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, ~p"/uploads", upload: @invalid_attrs)
assert html_response(conn, 200) =~ "New Upload"
end
end
describe "edit upload" do
setup [:create_upload]
test "renders form for editing chosen upload", %{conn: conn, upload: upload} do
conn = get(conn, ~p"/uploads/#{upload}/edit")
assert html_response(conn, 200) =~ "Edit Upload"
end
end
describe "update upload" do
setup [:create_upload]
test "redirects when data is valid", %{conn: conn, upload: upload} do
conn = put(conn, ~p"/uploads/#{upload}", upload: @update_attrs)
assert redirected_to(conn) == ~p"/uploads/#{upload}"
conn = get(conn, ~p"/uploads/#{upload}")
assert html_response(conn, 200) =~ "some updated filename"
end
test "renders errors when data is invalid", %{conn: conn, upload: upload} do
conn = put(conn, ~p"/uploads/#{upload}", upload: @invalid_attrs)
assert html_response(conn, 200) =~ "Edit Upload"
end
end
describe "delete upload" do
setup [:create_upload]
test "deletes chosen upload", %{conn: conn, upload: upload} do
conn = delete(conn, ~p"/uploads/#{upload}")
assert redirected_to(conn) == ~p"/uploads"
assert_error_sent 404, fn ->
get(conn, ~p"/uploads/#{upload}")
end
end
end
defp create_upload(_) do
upload = upload_fixture()
%{upload: upload}
end
end

@ -0,0 +1,113 @@
defmodule BrightWeb.UploadLiveTest do
use BrightWeb.ConnCase
import Phoenix.LiveViewTest
import Bright.StreamsFixtures
@create_attrs %{size: 42, filename: "some filename", content_type: "some content_type"}
@update_attrs %{size: 43, filename: "some updated filename", content_type: "some updated content_type"}
@invalid_attrs %{size: nil, filename: nil, content_type: nil}
defp create_upload(_) do
upload = upload_fixture()
%{upload: upload}
end
describe "Index" do
setup [:create_upload]
test "lists all uploads", %{conn: conn, upload: upload} do
{:ok, _index_live, html} = live(conn, ~p"/uploads")
assert html =~ "Listing Uploads"
assert html =~ upload.filename
end
test "saves new upload", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/uploads")
assert index_live |> element("a", "New Upload") |> render_click() =~
"New Upload"
assert_patch(index_live, ~p"/uploads/new")
assert index_live
|> form("#upload-form", upload: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#upload-form", upload: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/uploads")
html = render(index_live)
assert html =~ "Upload created successfully"
assert html =~ "some filename"
end
test "updates upload in listing", %{conn: conn, upload: upload} do
{:ok, index_live, _html} = live(conn, ~p"/uploads")
assert index_live |> element("#uploads-#{upload.id} a", "Edit") |> render_click() =~
"Edit Upload"
assert_patch(index_live, ~p"/uploads/#{upload}/edit")
assert index_live
|> form("#upload-form", upload: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#upload-form", upload: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/uploads")
html = render(index_live)
assert html =~ "Upload updated successfully"
assert html =~ "some updated filename"
end
test "deletes upload in listing", %{conn: conn, upload: upload} do
{:ok, index_live, _html} = live(conn, ~p"/uploads")
assert index_live |> element("#uploads-#{upload.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#uploads-#{upload.id}")
end
end
describe "Show" do
setup [:create_upload]
test "displays upload", %{conn: conn, upload: upload} do
{:ok, _show_live, html} = live(conn, ~p"/uploads/#{upload}")
assert html =~ "Show Upload"
assert html =~ upload.filename
end
test "updates upload within modal", %{conn: conn, upload: upload} do
{:ok, show_live, _html} = live(conn, ~p"/uploads/#{upload}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Upload"
assert_patch(show_live, ~p"/uploads/#{upload}/show/edit")
assert show_live
|> form("#upload-form", upload: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#upload-form", upload: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/uploads/#{upload}")
html = render(show_live)
assert html =~ "Upload updated successfully"
assert html =~ "some updated filename"
end
end
end

@ -4,9 +4,6 @@ defmodule Bright.PlatformsFixtures do
entities via the `Bright.Platforms` context.
"""
alias Bright.Platforms.Platform
alias Bright.Platforms.PlatformAlias
@doc """
Generate a platform.
"""
@ -55,7 +52,7 @@ defmodule Bright.PlatformsFixtures do
}
|> Bright.Platforms.create_platform()
{:ok, platform_alias} =
{:ok, _platform_alias} =
%{
url: "https://melody.buzz",
platform_id: platform.id
@ -112,4 +109,18 @@ defmodule Bright.PlatformsFixtures do
platform
end
@doc """
Generate a platform_alias.
"""
def platform_alias_fixture(attrs \\ %{}) do
{:ok, platform_alias} =
attrs
|> Enum.into(%{
url: "some url"
})
|> Bright.Platforms.create_platform_alias()
platform_alias
end
end

@ -36,4 +36,20 @@ defmodule Bright.StreamsFixtures do
vod
end
@doc """
Generate a upload.
"""
def upload_fixture(attrs \\ %{}) do
{:ok, upload} =
attrs
|> Enum.into(%{
content_type: "some content_type",
filename: "some filename",
size: 42
})
|> Bright.Streams.create_upload()
upload
end
end

@ -1,5 +1,4 @@
defmodule Bright.XPostsFixtures do
alias Bright.Socials.XPost
alias Bright.Socials
@moduledoc """
@ -149,7 +148,7 @@ defmodule Bright.XPostsFixtures do
{:ok, x_post} =
attrs
|> Enum.into(defaults)
|> XPost.create_x_post()
|> Socials.create_x_post()
x_post
end

@ -64,7 +64,15 @@ services:
# # - path: /home/cj/Documents/ueberauth_patreon
# # action: sync
# # target: /app/contrib/ueberauth_patreon
# uppy:
# image: transloadit/companion:latest
# container_name: uppy-companion
# env_file:
# - .env.development
# volumes:
# - uppy_data:/mnt/uppy-server-data
# ports:
# - '3020:3020'
db:
image: postgres:15
@ -96,3 +104,4 @@ services:
volumes:
pg_data:
cache:
uppy_data:

@ -91,6 +91,7 @@ resource "vultr_reserved_ip" "futureporn_tracker_ip" {
ip_type = "v4"
}
# Virtual Private Cloud for connecting many VPS together on a private network
# We use this network connection for app<->db comms.
resource "vultr_vpc2" "futureporn_vpc2" {
@ -155,6 +156,29 @@ resource "vultr_instance" "capture_vps" {
user_data = base64encode(var.vps_user_data)
}
# vultr instance with a GPU. experimental.
# resource "vultr_instance" "capture_vps" {
# count = 0
# hostname = "fp-cap-${count.index}"
# plan = "vcg-a16-2c-8g-2vram"
# region = "ord"
# backups = "disabled"
# ddos_protection = "false"
# # os_id = 1743
# image_id = "ubuntu-xfce"
# app_variables = {
# desktopuser = "cj_clippy"
# }
# enable_ipv6 = true
# vpc2_ids = [vultr_vpc2.futureporn_vpc2.id]
# label = "fp capture ${count.index}"
# tags = ["futureporn", "capture"]
# ssh_key_ids = [local.envs.VULTR_SSH_KEY_ID]
# user_data = base64encode(var.vps_user_data)
# }
resource "vultr_instance" "database" {
count = 1
hostname = "fp-db-${count.index}"
@ -254,6 +278,7 @@ resource "ansible_group" "capture" {
name = "capture"
}
resource "ansible_group" "bright" {
name = "bright"
}
@ -277,7 +302,7 @@ resource "ansible_group" "futureporn" {
"database",
"capture",
"bright",
"tracker",
"tracker"
]
}