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 + + + + + + + + + +
+
+
+ + +
+ Period +
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + + +
+
+
+ + + +
+ +
+
+ + +
+ + + \ 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; +}