-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
21a5fc7
commit 356d3f2
Showing
16 changed files
with
1,645 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,7 @@ nb-configuration.xml | |
# Local environment | ||
.env | ||
|
||
.misc | ||
notes.txt | ||
|
||
node_modules | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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><script src="lastfm.js" type="module"></script></code> | ||
|
||
<p>The script defines/registers the custom html-element <code><lastfm-tracks/></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><lastfm-tracks user="your username"></lastfm-tracks></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><lastfm-tracks user="your username" apikey="your own api key" interval="60"></lastfm-tracks></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><lastfm-tracks backend="/proxy-api" interval="35"></lastfm-tracks></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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: [] | ||
}); | ||
} | ||
|
||
}); |
Oops, something went wrong.