diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea2a866
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+._*
+Desktop.ini
+Thumbs.db
+
+/node_modules
+/dist
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a7f7173
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Brandon Bennett
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cd43c1c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Art-Gen
+
+An app to generate thumbnails for YouTube Art Tracks!
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..f082c2e
--- /dev/null
+++ b/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+Art Gen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..784cc30
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,27 @@
+{
+ "name": "art-gen",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "art-gen",
+ "devDependencies": {
+ "@types/jsmediatags": "^3.9.3"
+ }
+ },
+ "node_modules/@types/jsmediatags": {
+ "version": "3.9.3",
+ "resolved": "https://registry.npmjs.org/@types/jsmediatags/-/jsmediatags-3.9.3.tgz",
+ "integrity": "sha512-oNEPG+SI5E/VWK0x9JTWwkU+LmmOgW4tisCE4IrxiyNfzIRyg9kspNjaoqknpN9HUIexDvbD2/wbViw6TGIFgw==",
+ "dev": true
+ }
+ },
+ "dependencies": {
+ "@types/jsmediatags": {
+ "version": "3.9.3",
+ "resolved": "https://registry.npmjs.org/@types/jsmediatags/-/jsmediatags-3.9.3.tgz",
+ "integrity": "sha512-oNEPG+SI5E/VWK0x9JTWwkU+LmmOgW4tisCE4IrxiyNfzIRyg9kspNjaoqknpN9HUIexDvbD2/wbViw6TGIFgw==",
+ "dev": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9d0e6ba
--- /dev/null
+++ b/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "art-gen",
+ "devDependencies": {
+ "@types/jsmediatags": "^3.9.3"
+ }
+}
\ No newline at end of file
diff --git a/src/app.mjs b/src/app.mjs
new file mode 100644
index 0000000..cddac58
--- /dev/null
+++ b/src/app.mjs
@@ -0,0 +1,52 @@
+import { readTags, fromPicture } from "./jsmediatags.mjs";
+
+const demo = await fetch("../test/26.m4a")
+.then(response => response.blob());
+
+const thumbnail = await generateThumbnail(demo);
+
+/**
+ * Generates a video thumbnail for a given song file.
+ *
+ * @param { Blob } song
+*/
+export async function generateThumbnail(song){
+ const canvas = document.querySelector("canvas");
+ if (canvas === null){
+ throw new ReferenceError("Cannot find canvas");
+ }
+
+ const ctx = canvas.getContext("2d");
+ if (ctx === null){
+ throw new SyntaxError("Cannot create canvas context");
+ }
+
+ const tags = await readTags(song);
+ console.log(tags);
+
+ if (typeof tags.picture === "undefined"){
+ throw new TypeError("Cannot load artwork from song");
+ }
+
+ const image = await fromPicture(tags.picture);
+
+ const { naturalHeight } = image;
+ const { width, height } = canvas;
+
+ ctx.translate(0,height / 2);
+ ctx.translate(0,-naturalHeight / 2);
+
+ ctx.filter = "blur(30px)";
+
+ for (let i = 0; i < 16; i++){
+ ctx.drawImage(image,0,0,width,naturalHeight);
+ }
+
+ ctx.resetTransform();
+ ctx.filter = "none";
+
+ ctx.fillStyle = "rgb(0 0 0 / 0.7)";
+ ctx.fillRect(0,0,width,height);
+
+ ctx.drawImage(image,135,135,810,810);
+}
\ No newline at end of file
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000..4ec4c0e
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1,7 @@
+declare global {
+ interface Window {
+ jsmediatags: typeof import("jsmediatags");
+ }
+}
+
+export {};
\ No newline at end of file
diff --git a/src/image.mjs b/src/image.mjs
new file mode 100644
index 0000000..0307048
--- /dev/null
+++ b/src/image.mjs
@@ -0,0 +1,14 @@
+/**
+ * Loads an image from a specified URL.
+ *
+ * @param { string } src
+ * @param { HTMLImageElement } image
+*/
+export async function loadImage(src,image = new Image()){
+ await new Promise((resolve,reject) => {
+ image.addEventListener("load",resolve,{ once: true });
+ image.addEventListener("error",reject,{ once: true });
+ image.src = src;
+ });
+ return image;
+}
\ No newline at end of file
diff --git a/src/jsmediatags.mjs b/src/jsmediatags.mjs
new file mode 100644
index 0000000..57e92f7
--- /dev/null
+++ b/src/jsmediatags.mjs
@@ -0,0 +1,39 @@
+import { loadImage } from "./image.mjs";
+const { jsmediatags } = window;
+
+/**
+ * @typedef { import("jsmediatags/types").Tags } Tags
+ * @typedef { import("jsmediatags/types").PictureType } PictureType
+*/
+
+/**
+ * Reads the media tags for a given song file.
+ *
+ * @param { string | Blob } data Accepts either a URL string or a Blob object.
+ * @returns { Promise } Resolves to a Tags object.
+*/
+export async function readTags(data){
+ return new Promise((resolve,reject) => {
+ jsmediatags.read(data,{
+ onSuccess: ({ tags }) => resolve(tags),
+ onError: reject
+ });
+ });
+}
+
+/**
+ * Reads a Picture tag, and returns an equivalent Image object for that data.
+ *
+ * @param { PictureType } picture
+*/
+export async function fromPicture(picture){
+ try {
+ const { format: type = "", data = [] } = picture;
+ const media = new Blob([new Uint8Array(data)],{ type });
+ const source = window.URL.createObjectURL(media);
+
+ return loadImage(source);
+ } catch {
+ throw new TypeError("Could not construct an image from the picture data");
+ }
+}
\ No newline at end of file
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..375344a
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,22 @@
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+html {
+ width: 100%;
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ font-family: system-ui, sans-serif;
+}
+
+canvas {
+ width: 100%;
+ height: 100%;
+ display: block;
+ object-fit: contain;
+}
\ No newline at end of file
diff --git a/test/26.m4a b/test/26.m4a
new file mode 100644
index 0000000..e3ac6f7
Binary files /dev/null and b/test/26.m4a differ
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..8dcb5ae
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "rootDir": "./src",
+ "outDir": "./dist",
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "target": "ESNext",
+ "noEmit": true,
+ "strict": true
+ }
+}
\ No newline at end of file