Skip to content

Commit

Permalink
initial setup
Browse files Browse the repository at this point in the history
  • Loading branch information
StigNygaard committed Sep 4, 2024
1 parent 21a5fc7 commit 356d3f2
Show file tree
Hide file tree
Showing 16 changed files with 1,645 additions and 20 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ nb-configuration.xml
# Local environment
.env

.misc
notes.txt

node_modules
Expand Down
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
# lastfm-widgets
# 🔴 lastfm-widgets

work-in-progress...
See *Tracks* widget in action on https://lastfm-widgets.deno.dev/ and https://www.rockland.dk/. At the first site,
the plan is you should be able to try widget showing scrobbles from your own Last-fm account, and play with various
other configuration options. But that is not fully ready yet.

See *Tracks* widget in action on https://www.rockland.dk/.
As name of this repository hints, I might have more than one Last.fm widget planned for this space 🙂

Will post code soon, and a demo to play with is planned on https://lastfm-widgets.deno.dev/.
## The technical...

The *Tracks* widget itself is made as a *webcomponent* using pure standard web client-side technologies (no frameworks
or build tools needed). It can work "alone" communicating directly with Last.fm's Audioscrobbler v2 API, or it can be
supported by a custom backend "proxy-api". The latter is recommended because it makes it possible to implement
throttling of requests to Last.fm's API.

This repository not only holds the widget itself, but also a demo page and an example backend proxy-api. The
proxy-api is made in [Deno](https://deno.com/) (server-side typescript/javascript). Also, this repository is set
up as a [Deno Deploy](https://deno.com/deploy) project. Any updates in main-branch are immediately
deployed to https://lastfm-widgets.deno.dev/.

The backend code is my first simple experiments/experience with Deno. Comments are welcome if you have tips to make
better code or better use of Deno features. So far it is on purpose I have kept it pretty basic and avoided
libraries/frameworks. I like to start that way to get a feeling for the standard features.

#### /widgets/ folder

The widget frontend code. *All* that is needed for widget to work in *Demo* or *Basic* mode. See
[Releases](https://github.com/StigNygaard/lastfm-widgets/releases) to get latest "release-version" of this folder's
content. And see https://lastfm-widgets.deno.dev/ for an explanation of the modes the widget supports.

#### /demo/ folder

Frontend-code for the demo page seen on https://lastfm-widgets.deno.dev/

#### /proxy-api/ folder

Contains *audioscrobbler.ts*, an example backend proxy-api made with Deno. The proxy-api is used on demo page when
widget is in *Backend-supported* mode, but also (planned to be) used by widget on [rockland.dk](https://www.rockland.dk/).

#### /main.ts file

Basically the "web-server" or "router" for https://lastfm-widgets.deno.dev/, serving above-mentioned content.

Binary file added demo/android-chrome-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/android-chrome-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions demo/demo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
:root {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 15px;
line-height: 1.4;
}

.demo {
display: flex;
gap: 2rem;
padding: 2rem;
}
.info {
min-width: 22rem;
max-width: 42rem;
}
.widget {
}
.options pre {
overflow:auto;
font-size: 85%;
padding: 0 0 1em 0;
border-bottom: 1px #eee solid;
}

.resizeable {
border: none;
padding: 1.2rem;
width: 18em;
min-width: 14em;
height: 70vh;
min-height: 7em;
overflow: auto;
resize: both;
}
lastfm-tracks {
box-sizing: border-box;
border: 1px solid #000;
height: 100%;
}
button {
margin: 1em 0;
}
82 changes: 82 additions & 0 deletions demo/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@

function styleDefString(width, height) {
return `
lastfm-tracks {
box-sizing: border-box;
border: 1px solid #000;
width: ${width}px;
height: ${height}px;
}`;
}

/**
* Throttles the execution of a given function by a specified interval.
*
* @param {Function} func - The function to throttle.
* @param {number} interval - The interval in milliseconds.
* @returns {Function} - The throttled function.
*/
function throttle(func, interval) {
let timeout = null;
return function (...args) {
if (timeout) return;
const later = () => {
func.apply(this, args);
timeout = null;
}
timeout = setTimeout(later, interval);
}
}

/**
* Debounces a function, ensuring that it is only called after a certain delay has passed since the last invocation.
*
* @param {Function} func - The function to be debounced.
* @param {number} delay - The delay time in milliseconds.
* @returns {Function} - The debounced function.
*/
function debounce(func, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}

/**
* ResizeObserverCallback function
* @param {ResizeObserverEntry[]} [roea] - ResizeObserverEntry Array
*/
function updateStyleDef(roea) {
const styling = document.querySelector(('.options pre'));
const widget = document.querySelector('lastfm-tracks');
if (styling && widget) {
const { offsetWidth, offsetHeight } = widget;
styling.textContent = styleDefString(offsetWidth, offsetHeight);
}
}
/**
* A "throttled" ResizeObserverCallback function
*/
const handleResizedWidget = throttle(updateStyleDef, 100);

window.addEventListener(
'DOMContentLoaded',
function () { // TODO run when widget is inserted...
const widget = document.querySelector('lastfm-tracks');
const stopButton = document.querySelector('button');
const toggleDynaHeader = document.querySelector('input');
if (widget) {
stopButton?.addEventListener('click', () => {
widget.stopUpdating()
});
toggleDynaHeader?.addEventListener('change', () => {
widget.classList.toggle('dynaheader', this.checked);
});
new ResizeObserver(handleResizedWidget).observe(widget);
}
},
false
);
Binary file added demo/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/favicon.ico
Binary file not shown.
109 changes: 109 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en" xml:lang="en">
<head prefix="og: http://ogp.me/ns#">
<meta charset="UTF-8" />
<title>last.fm widget demo and play site</title>
<link rel="stylesheet" href="demo.css" />
<script src="widgets/lastfm.js" type="module"></script>
<script src="demo.js" type="module"></script>

<meta property="og:site_name" content="last.fm widget demo and play site" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://lastfm-widgets.deno.dev/" />
<meta name="description" content="Customizable last.fm scrobbles widget. Show your last played tracks on your homepage or blog using this easy to use and configurable website widget." />
<meta name="keywords" content="last.fm,lastfm,scrobbles,scrobble,scrobbling,audioscrobbler,playlist,tracks,tracklist,recently played,play history,listening to,now playing,widget,webcomponent,javascript,Stig Nygaard,music,web-development,deno,deno deploy" />
<meta name="dc.creator.name" content="Stig Nygaard" />
<meta name="dc.language" content="en" />
<meta name="author" content="Stig Nygaard" />
<meta name="publisher" content="Stig Nygaard" />
</head>
<body>

<div class="demo">
<div class="info">
<h1>🔴 Last.fm <em>Tracks</em> widget</h1>
<p><i>By <a href="https://www.rockland.dk/" title="Homepage of Stig Nygaard">Stig Nygaard</a>.</i></p>
<p><strong><em>BETA-VERSION - Work In Progress!...</em></strong></p>

<p>This page is a simple demonstration of
<a href="https://github.com/StigNygaard/lastfm-widgets" title="GitHub repository">my Last.fm <em>Tracks</em> widget</a>
showing recent "scrobbles" from a last.fm account. I plan to make this page a place where you can play with
configuration of the widget, but that is not fully ready yet. The widget itself is also still a "beta-version".
So for now, just some quick basics...</p>

<p>To use the widget, you need to import <code>lastfm.js</code> as a module. If you do that from html, remember
the <code>type</code> attribute:</p>

<code>&lt;script src="lastfm.js" type="module"&gt;&lt;/script&gt;</code>

<p>The script defines/registers the custom html-element <code>&lt;lastfm-tracks/&gt;</code>. Simply insert such
element on a webpage where you want a <em>Tracks</em> widget to be.</p>

<p>When creating a widget, the script will read the <code>tracks.css</code> stylesheet file, which it expects
to find at same location as the script file itself.</p>

<p>The widget has 3 modes. The <em>Demo</em> and <em>Basic</em> modes are "standalone" (pure frontend) modes
where widget communicates <em>directly</em> with last.fm's API. The third mode is <em>Backend-supported</em>
mode where you need to have a backend "proxy-api" that the widget can communicate with. The idea is that the
proxy-api should mirror/forward the two methods(*) from last.fm's audioscrobbler 2.0 api that the widget
needs to function. Your own backend proxy-api is a very good idea, if widget is to be placed on a page with
<em>more</em> than <em>absolute minimal</em> amount of traffic, because you can implement "throttling" in the
proxy-api to prevent overwhelming volumes of requests being sent directly to last.fm's API.</p>

<ol>
<li>
<h3>"Demo" mode</h3>
<p>
<code>&lt;lastfm-tracks user="your username"&gt;&lt;/lastfm-tracks&gt;</code>
</p>
<p>In the "standalone" <em>Demo</em> mode, the widget is "static". Latest "scrobbles" will be loaded upon
creation of the widget, but tracklist will <em>not</em> be refreshed after that.</p>
</li>
<li>
<h3>"Basic" mode</h3>
<p>
<code>&lt;lastfm-tracks user="your username" apikey="your own api key" interval="60"&gt;&lt;/lastfm-tracks&gt;</code>
</p>
<p>Adding your own API-key - which you can <a href="https://www.last.fm/api/authentication">get for free</a> -
puts the widget into the <em>Basic</em> mode where it supports periodically refresh of the tracks-list.
The <code>interval</code> attribute sets the widget's track-list refresh-rate in seconds.</p>
</li>
<li>
<h3>"Backend-supported" mode</h3>
<p>
<code>&lt;lastfm-tracks backend="/proxy-api" interval="35"&gt;&lt;/lastfm-tracks&gt;</code>
</p>
<p>Add the <code>backend</code> attribute pointing to a custom "proxy API" to enable <em>Backend-supported</em>
mode. Depending on your proxy-api, you could also add other attributes like <code>user</code> and
<code>apikey</code>, but you don't have to. Usually it will be smarter (and simpler) to have fixed values
for these defined in the backend.</p>
</li>
</ol>

<p>(*) The two API methods that needs to be available in a proxy-api for widget to work in
<em>Backend-supported</em> mode, are
<em><a href="https://www.last.fm/api/show/user.getInfo">user.getinfo</a></em> and
<em><a href="https://www.last.fm/api/show/user.getRecentTracks">user.getrecenttracks</a></em>.</p>

</div>
<div class="widget">
<div class="resizeable">
<!-- my own backend: -->
<lastfm-tracks backend="/proxy-api" interval="35" ></lastfm-tracks>
<!-- demo mode: -->
<!-- <lastfm-tracks user="rockland"></lastfm-tracks> -->
<!-- basic mode: -->
<!-- <lastfm-tracks user="rockland" interval="65" apikey="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ></lastfm-tracks> -->

</div>
<button>Stop updating</button>
</div>
<div class="options">
<pre>
</pre>
<p><label><input type="checkbox" /> "Dynamic" widget header</label></p>
</div>
</div>

</body>
</html>
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"tasks": {
"start": "deno run --allow-net --allow-env --allow-read=./.env main.ts"
"dev": "deno run --allow-net --allow-env --allow-read=./demo,./widgets,./.env --watch main.ts",
"start": "deno run --allow-net --allow-env --allow-read=./demo,./widgets,./.env main.ts"
},
"deploy": {
"exclude": [
Expand Down
56 changes: 41 additions & 15 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,48 @@
import {serveDir, serveFile} from "jsr:@std/http/file-server";
import "jsr:@std/dotenv/load";
import {serveDir} from 'jsr:@std/http/file-server';
import 'jsr:@std/dotenv/load';
import {audioscrobbler} from './proxy-api/audioscrobbler.ts';

/**
* SET DENO_FUTURE=1
* @run --allow-net --allow-env --allow-read <url>
*/

const password = Deno.env.get('PASSWORD');

console.log(`Password is ${password}`);
Deno.env.set("APP-START", (new Date()).toString());

Deno.serve((req: Request) => {

const pathname = new URL(req.url).pathname;
console.log(`Pathname is ${pathname}.`);

console.log(`App was started at ${Deno.env.get("APP-START")}`);

return new Response("Hello, World! I'm a Deno TEST page...");
// Deno.env.set('APP-START', (new Date()).toString());

Deno.serve(async (req: Request) => {

const url = new URL(req.url);
const pathname = url.pathname;

// The "Router"...
if (pathname === '/proxy-api' || pathname === '/proxy-api/') {
// The "proxy API":
const result = await audioscrobbler(url.searchParams, req.headers);
return new Response(result.body, result.options);
} else if (pathname.startsWith('/widgets/')) {
// The static served widgets code:
return serveDir(req, {
urlRoot: 'widgets',
fsRoot: 'widgets',
showDirListing: false,
showDotfiles: false,
showIndex: true, // index.html
enableCors: false, // CORS not allowed/enabled (no CORS headers)
quiet: false, // logging of errors
headers: []
});
} else {
// The static served demo-page:
return serveDir(req, {
urlRoot: '',
fsRoot: 'demo',
showDirListing: false,
showDotfiles: false,
showIndex: true, // index.html
enableCors: false, // CORS not allowed/enabled (no CORS headers)
quiet: false, // logging of errors
headers: []
});
}

});
Loading

0 comments on commit 356d3f2

Please sign in to comment.