diff --git a/functions.php b/functions.php
new file mode 100644
index 0000000..85e7654
--- /dev/null
+++ b/functions.php
@@ -0,0 +1,137 @@
+console.log('Debug Objects: " . $output . "' );";
+}
+
+// check if LastFM user exist
+function checkUserExist($lastfmUser, $apiKey)
+{
+ $query = "https://ws.audioscrobbler.com/2.0/?method=user.getinfo&user=$lastfmUser&api_key=$apiKey&format=json";
+ try {
+ $lastfmUserInfo = file_get_contents($query);
+
+ // Check for errors
+ if ($lastfmUserInfo === false) {
+ throw new Exception("Unable to fetch Last.fm user info.");
+ }
+
+ $lastfmUserInfoJson = json_decode($lastfmUserInfo, true);
+
+ if (isset($lastfmUserInfoJson['user']['name'])) {
+ return true;
+ }
+
+ } catch (Exception $e) {
+ // Handle the exception (log, display an error message, etc.)
+ // For demonstration purposes, we'll return a string with the error message
+ return $e->getMessage();
+ // return false;
+ }
+}
+
+// Fatch Top Albums
+function fetchtopalbums($lastfmuser, $apikey, $period, $limit) {
+
+ // create the url
+ // https://www.last.fm/api/show/user.gettopalbums
+ $method = "user.gettopalbums";
+ $apiurl = "https://ws.audioscrobbler.com/2.0/";
+
+ $query = "$apiurl?method=$method&user=$lastfmuser&period=$period&limit=$limit&api_key=$apikey&format=json";
+
+ $lastfmdata = file_get_contents($query);
+ $lastfmdatajson = json_decode($lastfmdata, true);
+ $topalbums = $lastfmdatajson['topalbums']['album'];
+
+ return $topalbums;
+}
+
+// create albums cover array
+function createAlbumsCoverArray($topAlbums)
+{
+ $albumsCoverUrlList = array();
+ foreach ($topAlbums as $topAlbum) {
+ // check if extralarge image exist
+ $extralarge_image_link = isset($topAlbum['image'][3]['#text']) && !empty($topAlbum['image'][3]['#text'])
+ ? parse_url($topAlbum['image'][3]['#text'])
+ : null;
+
+ if ($extralarge_image_link) {
+ $image_filename = pathinfo($extralarge_image_link['path'], PATHINFO_BASENAME);
+ $original_image_link = "https://" . $extralarge_image_link['host'] . "/i/u/" . $image_filename;
+ $albumsCoverUrlList[] = $original_image_link;
+ }
+ }
+
+ return $albumsCoverUrlList;
+}
+
+function createImagesFromUrls($urls)
+{
+
+ $images = array();
+
+ foreach ($urls as $url) {
+ $fileExtension = pathinfo($url, PATHINFO_EXTENSION);
+
+ switch ($fileExtension) {
+ case 'jpg':
+ $images[] = imagecreatefromjpeg($url);
+ break;
+ case 'png':
+ $images[] = imagecreatefrompng($url);
+ break;
+ case 'gif':
+ $images[] = imagecreatefromgif($url);
+ break;
+ // Add more cases for other supported image formats if needed
+ }
+ }
+
+ return $images;
+}
+
+function createPatchwork($imagesSideSize, $patchworkHeight, $patchworkWidth, $noborder, $cols, $rows, $covers)
+{
+
+ // $patchworkWidth = $imagesSideSize * $cols + ($cols - 1); // 299 is the max size of the Last.fm profile left column ;)
+ // $patchworkHeight = $imagesSideSize * $rows + ($rows - 1);
+
+ // create the "empty" patchwork
+ $patchwork = imagecreatetruecolor($patchworkWidth, $patchworkHeight);
+
+ if (!$noborder) {
+ // create a white color (reminds me of SDL ^^)
+ $white = imagecolorallocate($patchwork, 255, 255, 255);
+ // we fill our patchwork by the white color
+ imagefilltoborder($patchwork, 0, 0, $white, $white);
+ }
+
+ // now we "parse" our images in the patchwork, while resizing them :]
+ for ($i = 0; $i < $rows; $i++) {
+ for ($j = 0; $j < $cols; $j++) {
+ imagecopyresampled($patchwork, $covers[$cols * $i + $j], $j * $imagesSideSize + $j, $i * $imagesSideSize + $i, 0, 0, $imagesSideSize + intval($noborder), $imagesSideSize + intval($noborder), imagesx($covers[$cols * $i + $j]), imagesy($covers[$cols * $i + $j]));
+ }
+ }
+
+ return $patchwork;
+}
+
+function createImageJsonData($fileName, $PatchworkWidth, $PatchworkHeight)
+{
+ $response = [
+ 'imagePath' => $fileName,
+ 'width' => $PatchworkWidth,
+ 'height' => $PatchworkHeight,
+ ];
+
+ // header('Content-Type: application/json');
+ return json_encode($response);
+}
+?>
\ No newline at end of file
diff --git a/images/.gitkeep b/images/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..68e0172
--- /dev/null
+++ b/index.php
@@ -0,0 +1,121 @@
+
+
+
+
+
+ Last.fm top albums patchwork generator
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dynamyc Image link :
+
+
Copy
+
+
+
Static Image link :
+
+
Copy
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/patchwork.php b/patchwork.php
new file mode 100644
index 0000000..a8f3835
--- /dev/null
+++ b/patchwork.php
@@ -0,0 +1,106 @@
+load();
+
+$apiKey = $_ENV['LASTFM_API_KEY'];
+
+// GET Params;
+$lastfmUser = $_GET["username"];
+$period = $_GET["period"];
+$rows = $_GET["rows"];
+$cols = $_GET["cols"];
+$imagesSize = $_GET["imageSize"];
+$noborder = (bool)(isset($_GET["noborder"]) && $_GET["noborder"]);
+
+// return json data or image file
+$json = (bool)(isset($_GET["json"]) && $_GET["json"]);
+
+// Get 5 more albums incase there isn't an available
+// image for one of the requested albums #lazyhackftw
+$limit = ($cols * $rows) + 5;
+
+// Fallback if imagesSize is not set
+(isset($imagesSize)) ? $imagesSideSize = $imagesSize : $imagesSideSize = 99;
+
+// Calculate patchwork size
+$patchworkWidth = $imagesSideSize * $cols + ($cols - 1); // 299 is the max size of the Last.fm profile left column ;)
+$patchworkHeight = $imagesSideSize * $rows + ($rows - 1);
+
+do {
+ // check if username is valid
+ if (preg_match('/^[a-zA-Z0-9_.-]+$/', $lastfmUser) !== 1) {
+ $response = [
+ 'error' => "Invalid Username",
+ ];
+ break;
+ }
+ // check if username exist
+ if (!checkUserExist($lastfmUser, $apiKey) === true) {
+ $response = [
+ 'error' => checkUserExist($lastfmUser, $apiKey),
+ ];
+ break;
+ }
+
+ // Fetch top albums fron LastFM
+ $topAlbums = fetchtopalbums($lastfmUser, $apiKey, $period, $limit);
+
+ // Check if albums is not empty
+ if (empty($topAlbums)) {
+ $response = [
+ 'error' => "User does not have scrobbled any albums",
+ ];
+ break;
+ }
+
+ $border = $noborder ? "noborder" : "border";
+ // create Hash filename to avoid duplication
+ $topAlbumsDataHash = hash('md5',json_encode($topAlbums));
+ // $fileName = "images/$lastfmUser_$period_$rows_$cols_$imagesSize_$border-hash_$topAlbumsDataHash.jpg";
+ $fileName = "images/{$lastfmUser}_{$period}_{$rows}_{$cols}_{$imagesSize}_{$border}-hash_{$topAlbumsDataHash}.jpg";
+ // If file exist return existing data or image
+ if (file_exists($fileName)) {
+ $response = [
+ 'imagePath' => $fileName,
+ 'width' => $patchworkWidth,
+ 'height' => $patchworkHeight,
+ ];
+
+ // $patchwork = file_get_contents($fileName);
+ $patchwork = imagecreatefromjpeg($fileName);
+ // console_log($patchwork);
+ break;
+ }
+
+ // Else Generate a new patchwork
+ $albumsCovers = createAlbumsCoverArray($topAlbums);
+ $covers = createImagesFromUrls($albumsCovers);
+ $patchwork = createPatchwork($imagesSideSize, $patchworkHeight, $patchworkWidth, $noborder, $cols, $rows, $covers);
+ // console_log(gettype($patchwork));
+
+ // save the image into a file
+ imagejpeg($patchwork, $fileName);
+
+ $response = [
+ 'imagePath' => $fileName,
+ 'width' => $patchworkWidth,
+ 'height' => $patchworkHeight,
+ ];
+
+} while (0);
+
+// return json if requested else return image
+if ($json) {
+ // return json data
+ header('Content-Type: application/json');
+ echo json_encode($response);
+} else {
+ // display the image
+ header("Content-type: image/jpg");
+ imagejpeg($patchwork);
+}
diff --git a/scripts/main.js b/scripts/main.js
new file mode 100644
index 0000000..38ac2e8
--- /dev/null
+++ b/scripts/main.js
@@ -0,0 +1,79 @@
+function submitForm(event) {
+ event.preventDefault(); // Prevent the form from submitting in the traditional way
+ const resultContainer = document.getElementById('resultcontainer')
+ const usernameField = document.getElementById('username')
+ const submitbtn = document.getElementById('submitbtn')
+ const patchworkStaticLink = document.getElementById('patchworkStaticLink')
+ const patchworkDynLink = document.getElementById('patchworkDynLink')
+ const patchworkImg = document.getElementById('patchworkImg')
+ submitbtn.setAttribute("aria-busy", "true")
+ submitbtn.innerHTML = "Generating Patchwork, please wait…"
+ resultContainer.classList.add('hidden')
+ // resultContainer.innerHTML = 'Generating Patchwork, please wait…
'
+ // Fetch the form data
+ const formData = new FormData(event.target);
+ const patchworkDynamicParams = new URLSearchParams(formData).toString();
+
+ // add json true to url param to get json data instead of image
+ formData.append('json', 'true');
+ // Make an asynchronous request to patchwork.php
+ fetch(event.target.action + "?" + new URLSearchParams(formData), {
+ method: 'GET',
+ })
+ .then(response => response.json()) // Assuming patchwork.php returns text
+ .then(result => {
+
+ if (result.error) {
+ submitbtn.removeAttribute("aria-busy")
+ submitbtn.innerHTML = result.error
+ return
+ }
+
+ submitbtn.removeAttribute("aria-busy")
+ submitbtn.innerHTML = "Patchwork Generated !"
+
+ const baseUrl = location.protocol.concat("//").concat(window.location.host);
+ const patchworkDynamicUrl = baseUrl + "/patchwork.php?" + patchworkDynamicParams
+ const patchworkStaticUrl = baseUrl + "/" + result.imagePath;
+
+ // Update the result container with the response
+ patchworkStaticLink.innerHTML = patchworkStaticUrl;
+ patchworkStaticLink.setAttribute("href", patchworkStaticUrl );
+ patchworkDynLink.innerHTML = patchworkDynamicUrl;
+ patchworkDynLink.setAttribute("href", patchworkDynamicUrl);
+
+ patchworkImg.setAttribute("src", patchworkStaticUrl)
+ patchworkImg.setAttribute("height", result.height)
+ patchworkImg.setAttribute("width", result.width)
+
+ resultContainer.classList.remove('hidden')
+ })
+ .catch(error => console.error('Error:', error));
+}
+
+function copyToClipboard(event, elementId) {
+
+ const clickedElement = event.currentTarget;
+ const elementToCopy = document.getElementById(elementId);
+
+ // Select the text field
+ const copyText = elementToCopy.innerHTML;
+ console.log('copyText', copyText)
+ // copyText.setSelectionRange(0, 99999); // For mobile devices
+
+ // Copy the text inside the text field
+ navigator.clipboard.writeText(copyText);
+
+ // Alert the copied text
+ // alert("Copied the text: " + copyText);
+
+ clickedElement.innerHTML = "Copied !!"
+ clickedElement.classList.add('secondary');
+ clickedElement.classList.remove('contrast');
+ setTimeout(() => {
+ clickedElement.innerHTML = "Copy"
+ clickedElement.classList.add('contrast');
+ clickedElement.classList.remove('secondary');
+ }, 3000);
+
+}
\ No newline at end of file
diff --git a/styles/main.css b/styles/main.css
new file mode 100644
index 0000000..1416447
--- /dev/null
+++ b/styles/main.css
@@ -0,0 +1,67 @@
+form fieldset {
+ padding-bottom: 1em;
+}
+
+.hidden {
+ display: none !important;
+}
+
+#resultcontainer {
+ margin-left: auto;
+ margin-right: auto;
+ padding-left: 2em;
+ padding-right: 2em;
+ display: flex;
+ flex-direction: column;
+ /* justify-content: center; */
+ /* align-items: center; */
+ border-radius: 5px;
+ /* border: 1px; */
+ background-color: var(--card-background-color);
+ /* border-color: rgb(0, 211, 35); */
+ /* border-style: solid; */
+ width: 100%;
+}
+#resultcontainer img {
+ padding: 5px;
+ margin-top: var(--block-spacing-vertical);
+ margin-bottom: var(--block-spacing-vertical);
+}
+.field-title {
+ padding-top: var(--spacing);
+ padding-bottom: var(--spacing);
+ margin-top: var(--spacing);
+ text-decoration: underline;
+ width: 100%;
+ /* text-decoration-color: beige; */
+}
+.img-link {
+ /* margin-top: 2em; */
+ display: flex;
+ width: 100%;
+ margin-bottom: var(--spacing);;
+ margin-top: var(--spacing);;
+}
+
+.img-link > a {
+ padding: var(--spacing);
+ width: 100%;
+ border-radius: 5px;
+ border: 1px;
+ border-style: solid;
+ border-color: var(--primary-hover);
+ background-color: var(--code-background-color);
+ font-family: monospace;
+ white-space: nowrap;
+ overflow-x: auto;
+
+}
+
+.copy-btn {
+
+}
+.patchwork {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}