diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index f25a07c..a906e41 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -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 diff --git a/apps/bright/assets/css/app.scss b/apps/bright/assets/css/app.scss index 794863f..7fa05a6 100644 --- a/apps/bright/assets/css/app.scss +++ b/apps/bright/assets/css/app.scss @@ -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 { diff --git a/apps/bright/assets/css/dropzone.scss b/apps/bright/assets/css/dropzone.scss new file mode 100644 index 0000000..e112eef --- /dev/null +++ b/apps/bright/assets/css/dropzone.scss @@ -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 +} \ No newline at end of file diff --git a/apps/bright/assets/js/app.js b/apps/bright/assets/js/app.js index 3650eb4..79cfff9 100644 --- a/apps/bright/assets/js/app.js +++ b/apps/bright/assets/js/app.js @@ -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()) diff --git a/apps/bright/assets/js/hooks/index.js b/apps/bright/assets/js/hooks/index.js index 5709c05..febeb99 100644 --- a/apps/bright/assets/js/hooks/index.js +++ b/apps/bright/assets/js/hooks/index.js @@ -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 \ No newline at end of file diff --git a/apps/bright/assets/js/hooks/uppy_hook.js b/apps/bright/assets/js/hooks/uppy_hook.js new file mode 100644 index 0000000..30a92e6 --- /dev/null +++ b/apps/bright/assets/js/hooks/uppy_hook.js @@ -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 \ No newline at end of file diff --git a/apps/bright/assets/js/uploaders.js b/apps/bright/assets/js/uploaders.js new file mode 100644 index 0000000..9ec78ce --- /dev/null +++ b/apps/bright/assets/js/uploaders.js @@ -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; \ No newline at end of file diff --git a/apps/bright/assets/package-lock.json b/apps/bright/assets/package-lock.json index 4f04720..e2edf3b 100644 --- a/apps/bright/assets/package-lock.json +++ b/apps/bright/assets/package-lock.json @@ -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" + } } } } diff --git a/apps/bright/assets/package.json b/apps/bright/assets/package.json index 48ebcbe..e75d469 100644 --- a/apps/bright/assets/package.json +++ b/apps/bright/assets/package.json @@ -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" } diff --git a/apps/bright/config/config.exs b/apps/bright/config/config.exs index f0a8de2..f0898e8 100644 --- a/apps/bright/config/config.exs +++ b/apps/bright/config/config.exs @@ -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/*), diff --git a/apps/bright/lib/bright/oban_workers/process_posts.ex b/apps/bright/lib/bright/oban_workers/process_posts.ex index 9d1fa5d..a47300c 100644 --- a/apps/bright/lib/bright/oban_workers/process_posts.ex +++ b/apps/bright/lib/bright/oban_workers/process_posts.ex @@ -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( diff --git a/apps/bright/lib/bright/platforms.ex b/apps/bright/lib/bright/platforms.ex index 3f842b1..53a7cc9 100644 --- a/apps/bright/lib/bright/platforms.ex +++ b/apps/bright/lib/bright/platforms.ex @@ -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 diff --git a/apps/bright/lib/bright/socials/x_post.ex b/apps/bright/lib/bright/socials/x_post.ex index 1b8b95a..ba58b73 100644 --- a/apps/bright/lib/bright/socials/x_post.ex +++ b/apps/bright/lib/bright/socials/x_post.ex @@ -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)} ] diff --git a/apps/bright/lib/bright/streams.ex b/apps/bright/lib/bright/streams.ex index 703c965..2b6e32f 100644 --- a/apps/bright/lib/bright/streams.ex +++ b/apps/bright/lib/bright/streams.ex @@ -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 diff --git a/apps/bright/lib/bright/streams/upload.ex b/apps/bright/lib/bright/streams/upload.ex new file mode 100644 index 0000000..14b7ced --- /dev/null +++ b/apps/bright/lib/bright/streams/upload.ex @@ -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 diff --git a/apps/bright/lib/bright/utils.ex b/apps/bright/lib/bright/utils.ex new file mode 100644 index 0000000..d8c210b --- /dev/null +++ b/apps/bright/lib/bright/utils.ex @@ -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 diff --git a/apps/bright/lib/bright_web.ex b/apps/bright/lib/bright_web.ex index e80a48b..32794bb 100644 --- a/apps/bright/lib/bright_web.ex +++ b/apps/bright/lib/bright_web.ex @@ -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 diff --git a/apps/bright/lib/bright_web/components/core_components.ex b/apps/bright/lib/bright_web/components/core_components.ex index 8ceea4d..c8b7c61 100644 --- a/apps/bright/lib/bright_web/components/core_components.ex +++ b/apps/bright/lib/bright_web/components/core_components.ex @@ -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 diff --git a/apps/bright/lib/bright_web/components/layouts/app.html.heex b/apps/bright/lib/bright_web/components/layouts/app.html.heex index 9113baf..bafb346 100644 --- a/apps/bright/lib/bright_web/components/layouts/app.html.heex +++ b/apps/bright/lib/bright_web/components/layouts/app.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/components/svg_icon.ex b/apps/bright/lib/bright_web/components/svg_icon.ex deleted file mode 100644 index 6e4e435..0000000 --- a/apps/bright/lib/bright_web/components/svg_icon.ex +++ /dev/null @@ -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 diff --git a/apps/bright/lib/bright_web/components/svg_icon/icon.ex b/apps/bright/lib/bright_web/components/svg_icon/icon.ex deleted file mode 100644 index ef02f8f..0000000 --- a/apps/bright/lib/bright_web/components/svg_icon/icon.ex +++ /dev/null @@ -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 diff --git a/apps/bright/lib/bright_web/controllers/page_controller.ex b/apps/bright/lib/bright_web/controllers/page_controller.ex index dabebfd..1d874d6 100644 --- a/apps/bright/lib/bright_web/controllers/page_controller.ex +++ b/apps/bright/lib/bright_web/controllers/page_controller.ex @@ -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 diff --git a/apps/bright/lib/bright_web/controllers/page_html/about.html.heex b/apps/bright/lib/bright_web/controllers/page_html/about.html.heex index 004b3e5..79f33da 100644 --- a/apps/bright/lib/bright_web/controllers/page_html/about.html.heex +++ b/apps/bright/lib/bright_web/controllers/page_html/about.html.heex @@ -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 doesn’t 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. diff --git a/apps/bright/lib/bright_web/controllers/page_html/api.html.heex b/apps/bright/lib/bright_web/controllers/page_html/api.html.heex index 052c731..380612f 100644 --- a/apps/bright/lib/bright_web/controllers/page_html/api.html.heex +++ b/apps/bright/lib/bright_web/controllers/page_html/api.html.heex @@ -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> \ No newline at end of file + <.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> diff --git a/apps/bright/lib/bright_web/controllers/page_html/home.html.heex b/apps/bright/lib/bright_web/controllers/page_html/home.html.heex index d474945..4db45ba 100644 --- a/apps/bright/lib/bright_web/controllers/page_html/home.html.heex +++ b/apps/bright/lib/bright_web/controllers/page_html/home.html.heex @@ -15,9 +15,7 @@ </p> <i> <p class="mt-3"> - <% # <.icon name="circle-exclamation" class="icon h-4 w-4" /> %> - - (Yeah, we’re still testing.) check back each week for updates. + <% # <.icon name="circle-exclamation" class="icon h-4 w-4" /> %> (Yeah, we’re still testing.) check back each week for updates. </p> </i> diff --git a/apps/bright/lib/bright_web/controllers/page_html/profile.html.heex b/apps/bright/lib/bright_web/controllers/page_html/profile.html.heex deleted file mode 100644 index 22c1119..0000000 --- a/apps/bright/lib/bright_web/controllers/page_html/profile.html.heex +++ /dev/null @@ -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 %> diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_controller.ex b/apps/bright/lib/bright_web/controllers/platform_alias_controller.ex new file mode 100644 index 0000000..1220b38 --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/platform_alias_controller.ex @@ -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 diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html.ex b/apps/bright/lib/bright_web/controllers/platform_alias_html.ex new file mode 100644 index 0000000..c725e19 --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/platform_alias_html.ex @@ -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 diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html/edit.html.heex b/apps/bright/lib/bright_web/controllers/platform_alias_html/edit.html.heex new file mode 100644 index 0000000..9ca852a --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/platform_alias_html/edit.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html/index.html.heex b/apps/bright/lib/bright_web/controllers/platform_alias_html/index.html.heex new file mode 100644 index 0000000..4ae67ef --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/platform_alias_html/index.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html/new.html.heex b/apps/bright/lib/bright_web/controllers/platform_alias_html/new.html.heex new file mode 100644 index 0000000..f739322 --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/platform_alias_html/new.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html/platform_alias_form.html.heex b/apps/bright/lib/bright_web/controllers/platform_alias_html/platform_alias_form.html.heex new file mode 100644 index 0000000..e6e3225 --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/platform_alias_html/platform_alias_form.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/platform_alias_html/show.html.heex b/apps/bright/lib/bright_web/controllers/platform_alias_html/show.html.heex new file mode 100644 index 0000000..8704aa7 --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/platform_alias_html/show.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/platform_html/index.html.heex b/apps/bright/lib/bright_web/controllers/platform_html/index.html.heex index 3d643c4..f3b1f9d 100644 --- a/apps/bright/lib/bright_web/controllers/platform_html/index.html.heex +++ b/apps/bright/lib/bright_web/controllers/platform_html/index.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/platform_html/platform_form.html.heex b/apps/bright/lib/bright_web/controllers/platform_html/platform_form.html.heex index e3478ff..dc8d573 100644 --- a/apps/bright/lib/bright_web/controllers/platform_html/platform_form.html.heex +++ b/apps/bright/lib/bright_web/controllers/platform_html/platform_form.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/platform_html/show.html.heex b/apps/bright/lib/bright_web/controllers/platform_html/show.html.heex index c94efa7..cc7b8ba 100644 --- a/apps/bright/lib/bright_web/controllers/platform_html/show.html.heex +++ b/apps/bright/lib/bright_web/controllers/platform_html/show.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex b/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex index aa48eb7..3c1b338 100644 --- a/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex +++ b/apps/bright/lib/bright_web/controllers/stream_html/index.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/upload_controller.ex b/apps/bright/lib/bright_web/controllers/upload_controller.ex new file mode 100644 index 0000000..186132d --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/upload_controller.ex @@ -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 diff --git a/apps/bright/lib/bright_web/controllers/upload_html.ex b/apps/bright/lib/bright_web/controllers/upload_html.ex new file mode 100644 index 0000000..e885bbe --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/upload_html.ex @@ -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 diff --git a/apps/bright/lib/bright_web/controllers/upload_html/edit.html.heex b/apps/bright/lib/bright_web/controllers/upload_html/edit.html.heex new file mode 100644 index 0000000..575822f --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/upload_html/edit.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/upload_html/index.html.heex b/apps/bright/lib/bright_web/controllers/upload_html/index.html.heex new file mode 100644 index 0000000..e329877 --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/upload_html/index.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/upload_html/new.html.heex b/apps/bright/lib/bright_web/controllers/upload_html/new.html.heex new file mode 100644 index 0000000..b42d0b6 --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/upload_html/new.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/upload_html/show.html.heex b/apps/bright/lib/bright_web/controllers/upload_html/show.html.heex new file mode 100644 index 0000000..d015d82 --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/upload_html/show.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/upload_html/upload_form.html.heex b/apps/bright/lib/bright_web/controllers/upload_html/upload_form.html.heex new file mode 100644 index 0000000..d5b2bfe --- /dev/null +++ b/apps/bright/lib/bright_web/controllers/upload_html/upload_form.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/controllers/vtuber_html/index.html.heex b/apps/bright/lib/bright_web/controllers/vtuber_html/index.html.heex index e08ae9a..a5c2161 100644 --- a/apps/bright/lib/bright_web/controllers/vtuber_html/index.html.heex +++ b/apps/bright/lib/bright_web/controllers/vtuber_html/index.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/live/upload_live/form_component.ex b/apps/bright/lib/bright_web/live/upload_live/form_component.ex new file mode 100644 index 0000000..e10549e --- /dev/null +++ b/apps/bright/lib/bright_web/live/upload_live/form_component.ex @@ -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 diff --git a/apps/bright/lib/bright_web/live/upload_live/index.ex b/apps/bright/lib/bright_web/live/upload_live/index.ex new file mode 100644 index 0000000..16fda4e --- /dev/null +++ b/apps/bright/lib/bright_web/live/upload_live/index.ex @@ -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 diff --git a/apps/bright/lib/bright_web/live/upload_live/show.ex b/apps/bright/lib/bright_web/live/upload_live/show.ex new file mode 100644 index 0000000..5ada707 --- /dev/null +++ b/apps/bright/lib/bright_web/live/upload_live/show.ex @@ -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 diff --git a/apps/bright/lib/bright_web/live/upload_live/show.html.heex b/apps/bright/lib/bright_web/live/upload_live/show.html.heex new file mode 100644 index 0000000..b861be5 --- /dev/null +++ b/apps/bright/lib/bright_web/live/upload_live/show.html.heex @@ -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> diff --git a/apps/bright/lib/bright_web/live/upload_live/simple_s3_upload.ex b/apps/bright/lib/bright_web/live/upload_live/simple_s3_upload.ex new file mode 100644 index 0000000..d13ca7c --- /dev/null +++ b/apps/bright/lib/bright_web/live/upload_live/simple_s3_upload.ex @@ -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 diff --git a/apps/bright/lib/bright_web/live/upload_live/upload_live.ex b/apps/bright/lib/bright_web/live/upload_live/upload_live.ex new file mode 100644 index 0000000..f5b1ff3 --- /dev/null +++ b/apps/bright/lib/bright_web/live/upload_live/upload_live.ex @@ -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 diff --git a/apps/bright/lib/bright_web/router.ex b/apps/bright/lib/bright_web/router.ex index 7585711..83e69e4 100644 --- a/apps/bright/lib/bright_web/router.ex +++ b/apps/bright/lib/bright_web/router.ex @@ -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 diff --git a/apps/bright/mix.exs b/apps/bright/mix.exs index 5fa75af..f382cd0 100644 --- a/apps/bright/mix.exs +++ b/apps/bright/mix.exs @@ -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 diff --git a/apps/bright/mix.lock b/apps/bright/mix.lock index ae87ca3..b2c44d7 100644 --- a/apps/bright/mix.lock +++ b/apps/bright/mix.lock @@ -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"}, diff --git a/apps/bright/priv/repo/migrations/20250311210011_add_x_posts.exs b/apps/bright/priv/repo/migrations/20250311210011_add_x_posts.exs index 0070d6d..4aea450 100644 --- a/apps/bright/priv/repo/migrations/20250311210011_add_x_posts.exs +++ b/apps/bright/priv/repo/migrations/20250311210011_add_x_posts.exs @@ -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 diff --git a/apps/bright/priv/repo/migrations/20250320193227_create_uploads.exs b/apps/bright/priv/repo/migrations/20250320193227_create_uploads.exs new file mode 100644 index 0000000..3185746 --- /dev/null +++ b/apps/bright/priv/repo/migrations/20250320193227_create_uploads.exs @@ -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 diff --git a/apps/bright/priv/repo/migrations/20250323033920_create_uploads.exs b/apps/bright/priv/repo/migrations/20250323033920_create_uploads.exs new file mode 100644 index 0000000..0f5271c --- /dev/null +++ b/apps/bright/priv/repo/migrations/20250323033920_create_uploads.exs @@ -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 diff --git a/apps/bright/priv/static/favicon-5b6eb29e50adf329d96e0eeca8d2d0ee.ico b/apps/bright/priv/static/favicon-5b6eb29e50adf329d96e0eeca8d2d0ee.ico new file mode 100644 index 0000000..04e664e Binary files /dev/null and b/apps/bright/priv/static/favicon-5b6eb29e50adf329d96e0eeca8d2d0ee.ico differ diff --git a/apps/bright/test/bright/images_test.exs b/apps/bright/test/bright/images_test.exs index 9bb7d14..91c15f0 100644 --- a/apps/bright/test/bright/images_test.exs +++ b/apps/bright/test/bright/images_test.exs @@ -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)}" diff --git a/apps/bright/test/bright/oban_workers/process_posts_test.exs b/apps/bright/test/bright/oban_workers/process_posts_test.exs index a1ae486..6fdbcb9 100644 --- a/apps/bright/test/bright/oban_workers/process_posts_test.exs +++ b/apps/bright/test/bright/oban_workers/process_posts_test.exs @@ -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 diff --git a/apps/bright/test/bright/oban_workers/process_vod_test.exs b/apps/bright/test/bright/oban_workers/process_vod_test.exs index 0d7aad4..ed2b903 100644 --- a/apps/bright/test/bright/oban_workers/process_vod_test.exs +++ b/apps/bright/test/bright/oban_workers/process_vod_test.exs @@ -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 diff --git a/apps/bright/test/bright/platforms_test.exs b/apps/bright/test/bright/platforms_test.exs index 2f53504..3038fc2 100644 --- a/apps/bright/test/bright/platforms_test.exs +++ b/apps/bright/test/bright/platforms_test.exs @@ -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 diff --git a/apps/bright/test/bright/socials/x_post_test.exs b/apps/bright/test/bright/socials/x_post_test.exs index ad405ce..c29946d 100644 --- a/apps/bright/test/bright/socials/x_post_test.exs +++ b/apps/bright/test/bright/socials/x_post_test.exs @@ -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 diff --git a/apps/bright/test/bright/streams_test.exs b/apps/bright/test/bright/streams_test.exs index 2e9875f..14277e5 100644 --- a/apps/bright/test/bright/streams_test.exs +++ b/apps/bright/test/bright/streams_test.exs @@ -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 diff --git a/apps/bright/test/bright_web/controllers/platform_alias_controller_test.exs b/apps/bright/test/bright_web/controllers/platform_alias_controller_test.exs new file mode 100644 index 0000000..40c8655 --- /dev/null +++ b/apps/bright/test/bright_web/controllers/platform_alias_controller_test.exs @@ -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 diff --git a/apps/bright/test/bright_web/controllers/upload_controller_test.exs b/apps/bright/test/bright_web/controllers/upload_controller_test.exs new file mode 100644 index 0000000..d61ef8b --- /dev/null +++ b/apps/bright/test/bright_web/controllers/upload_controller_test.exs @@ -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 diff --git a/apps/bright/test/bright_web/live/upload_live_test.exs b/apps/bright/test/bright_web/live/upload_live_test.exs new file mode 100644 index 0000000..3e789d8 --- /dev/null +++ b/apps/bright/test/bright_web/live/upload_live_test.exs @@ -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'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'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'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 diff --git a/apps/bright/test/support/fixtures/platforms_fixtures.ex b/apps/bright/test/support/fixtures/platforms_fixtures.ex index c865ce7..6db525a 100644 --- a/apps/bright/test/support/fixtures/platforms_fixtures.ex +++ b/apps/bright/test/support/fixtures/platforms_fixtures.ex @@ -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 diff --git a/apps/bright/test/support/fixtures/streams_fixtures.ex b/apps/bright/test/support/fixtures/streams_fixtures.ex index 89a5a08..924e084 100644 --- a/apps/bright/test/support/fixtures/streams_fixtures.ex +++ b/apps/bright/test/support/fixtures/streams_fixtures.ex @@ -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 diff --git a/apps/bright/test/support/fixtures/x_posts_fixtures.ex b/apps/bright/test/support/fixtures/x_posts_fixtures.ex index c230a8e..5f0bc39 100644 --- a/apps/bright/test/support/fixtures/x_posts_fixtures.ex +++ b/apps/bright/test/support/fixtures/x_posts_fixtures.ex @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 42f18c7..3fc5e7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/terraform/main.tf b/terraform/main.tf index 270563e..82ae4a1 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -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" ] }