Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lastx@swud: Initial submission #1406

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions lastx@swud/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<<<<<<< HEAD
# LastfmNowPDesklet
=======
# Last.fm Desklet for Cinnamon

A **Cinnamon Desklet** that fetches and displays your currently playing or last listened track from **Last.fm**, including:

- **Song Title**
- **Artist Name**
- **Album Cover**
- **Timestamp (Last Played)**

## Features
- Automatically fetches the latest played track from Last.fm
- Displays song details in a stylish overlay
- Updates at a configurable interval
- Fetches and sets the album art as the desklet's background
- Lightweight and simple to use

## Installation
1. **Download** the desklet files and place them in your Cinnamon desklet directory:
```sh
~/.local/share/cinnamon/desklets/lastx@swud/
```
2. **Enable** the desklet:
- Right-click on the desktop → Add Desklets
- Find **Last.fm Stats** and add it to your desktop
3. **Configure Settings**:
- Open desklet settings
- Enter your **Last.fm username**
- Adjust the **update interval** if needed

## Configuration
The desklet uses a **settings-schema.json** file for easy customization:

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `lastfm_user` | String | `""` | Your Last.fm username |
| `update_interval` | Number | `60` | How often to fetch updates (in seconds) |

## Dependencies
This desklet requires **GNOME Shell** and Cinnamon's **Desklet API**. Ensure Cinnamon and its components are installed.

## Troubleshooting
- If the desklet does not update:
- Check that your Last.fm username is correctly set
- Increase the update interval if requests are failing
- Ensure you have an active internet connection

>>>>>>> cd96aff (v1)
304 changes: 304 additions & 0 deletions lastx@swud/files/lastx@swud/desklet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
const Desklet = imports.ui.desklet;
const St = imports.gi.St;
const Mainloop = imports.mainloop;
const Settings = imports.ui.settings;
const Soup = imports.gi.Soup;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const ByteArray = imports.byteArray;
const Lang = imports.lang;

class LastFmDesklet extends Desklet.Desklet {
constructor(metadata, desklet_id) {
super(metadata, desklet_id);
this._timeout = null;
this._httpSession = new Soup.Session();
this._httpSession.set_timeout(10);
this._currentArtPath = null;
this._initSettings(desklet_id);
this._initUI();
this._updateTrack();
}

_initSettings(desklet_id) {
this.settings = new Settings.DeskletSettings(this, "lastx@swud", desklet_id);
this.settings.bind("lastfm_user", "lastfmUser", this._onSettingsChanged.bind(this));
this.settings.bind("update_interval", "updateInterval", this._onSettingsChanged.bind(this));
}

_initUI() {
this.layout = new St.BoxLayout({
vertical: true
});

this.overlay = new St.BoxLayout({
vertical: true,
x_expand: true,
y_expand: true,
x_align: St.Align.START,
y_align: St.Align.START,
style: 'background-color: rgba(0, 0, 0, 0.4); padding: 20px; border-radius: 12px;'
});
// Profile container
this.profileContainer = new St.BoxLayout({
style: 'spacing: 10px; margin-bottom: 15px;'
});


// Profile text container
this.profileTextContainer = new St.BoxLayout({
vertical: true,
style: 'spacing: 2px;'
});

this.titleLabel = new St.Label({
text: "Last.fm Recently Played",
style: 'font-size: 14px; color: ;'
});

this.usernameLabel = new St.Label({
text: this.lastfmUser || "No username set",
style: 'font-size: 16px; color: #1db954;'
});

this.trackLabel = new St.Label({
text: "Loading...",
style: 'font-size: 20px; font-weight: bold; color: #ffffff;'
});

this.artistLabel = new St.Label({
text: "",
style: 'font-size: 18px; color: #b3b3b3;'
});

this.timestampLabel = new St.Label({
text: "",
style: 'font-size: 14px; color: #b3b3b3; margin-top: 5px;'
});

// Assemble the profile section
this.profileTextContainer.add(this.titleLabel);
this.profileTextContainer.add(this.usernameLabel);
this.profileContainer.add(this.profileTextContainer);

// Add all components to overlay
this.overlay.add(this.profileContainer);
this.overlay.add(this.trackLabel);
this.overlay.add(this.artistLabel);
this.overlay.add(this.timestampLabel);

this.layout.add(this.overlay);
this.setContent(this.layout);
}

_onSettingsChanged() {
if (this._timeout) {
Mainloop.source_remove(this._timeout);
}

this.usernameLabel.set_text(this.lastfmUser || "No username set");
this.updateInterval = Math.max(this.updateInterval, 30);
this._updateTrack();
}

async _updateTrack() {
try {
const trackData = await this._fetchLastFmData();
if (!trackData) {
this._updateLabels("No track playing", "");
this._setDefaultBackground();
} else {
this._updateUI(trackData);
}
} catch (error) {
global.logError(`LastFm Desklet Error: ${error.message}`);
this._updateLabels("Error updating track", "");
this._setDefaultBackground();
}

this._timeout = Mainloop.timeout_add_seconds(this.updateInterval, () => {
this._updateTrack();
return false;
});
}

async _fetchLastFmData() {
if (!this.lastfmUser) return null;

const url = `https://klaxonx.vercel.app/api/lastfm?user=${encodeURIComponent(this.lastfmUser)}`;
const message = Soup.Message.new('GET', url);

try {
const response = await new Promise((resolve, reject) => {
this._httpSession.send_and_read_async(message, GLib.PRIORITY_DEFAULT, null, (session, result) => {
try {
const bytes = session.send_and_read_finish(result);
if (!bytes) throw new Error('No data received');
resolve(ByteArray.toString(bytes.get_data()));
} catch (error) {
reject(error);
}
});
});

const data = JSON.parse(response);
if (data.tracks?.[0]) {
const track = data.tracks[0];

// Ensure timestamp is properly formatted
track.timestamp = this._formatTimestamp(track.timestamp);

return track;
}
return null;
} catch (error) {
global.logError(`LastFm Data Fetch Error: ${error.message}`);
return null;
}
}

_formatTimestamp(timestamp) {
if (!timestamp) return "Unknown time";

const trackDate = new Date(timestamp);
if (isNaN(trackDate.getTime())) {
global.logError(`Invalid timestamp: ${timestamp}`);
return "Unknown time";
}

const now = new Date();
const diff = now - trackDate;

if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;

return trackDate.toLocaleDateString();
}

_updateLabels(trackText, artistText) {
this.trackLabel.set_text(trackText);
this.artistLabel.set_text(artistText);
}

_updateUI(track) {
this._updateLabels(
track.title || "Unknown Track",
track.artist ? `by ${track.artist}` : ""
);

// Ensure the timestampLabel is updated with the correct timestamp
this.timestampLabel.set_text(track.timestamp || "");

if (track.coverArt) {
this._setAlbumArt(track.coverArt);
} else {
this._setDefaultBackground();
}

if (track.userImage) {
this._updateProfileImage(track.userImage);
}
}

async _setAlbumArt(url) {
try {
const filePath = await this._fetchAlbumArt(url);
this._currentArtPath = filePath;
this._applyBackground(filePath);
} catch (error) {
global.logError(`Album Art Error: ${error.message}`);
this._setDefaultBackground();
}
}

_applyBackground(filePath) {
this.layout.set_style(`
background-image: url("file://${filePath}");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
min-width: 400px;
min-height: 200px;
border-radius: 12px;
`);
}

_setDefaultBackground() {
this.layout.set_style(`
background: linear-gradient(45deg, #1a1a1a, #2a2a2a);
min-width: 400px;
min-height: 200px;
border-radius: 12px;
`);
}

async _updateProfileImage(url) {
if (!url) {
this.profileImage.set_icon_name('avatar-default');
return;
}

try {
const filePath = await this._fetchAlbumArt(url);
this.profileImage.set_gicon(Gio.icon_new_for_string(filePath));
} catch (error) {
this.profileImage.set_icon_name('avatar-default');
}
}

async _fetchAlbumArt(url) {
const timestamp = Date.now();
const filePath = `${GLib.get_tmp_dir()}/lastfm_cover_${timestamp}.jpg`;
const message = Soup.Message.new('GET', url);

try {
const data = await new Promise((resolve, reject) => {
this._httpSession.send_and_read_async(message, GLib.PRIORITY_DEFAULT, null, (session, result) => {
try {
const bytes = session.send_and_read_finish(result);
if (!bytes) throw new Error('No image data received');
resolve(bytes.get_data());
} catch (error) {
reject(error);
}
});
});

const file = Gio.File.new_for_path(filePath);
const stream = file.replace(null, false, Gio.FileCreateFlags.NONE, null);
stream.write_all(data, null);
stream.close(null);

if (this._currentArtPath && this._currentArtPath !== filePath) {
this._deleteFile(this._currentArtPath);
}

return filePath;
} catch (error) {
throw new Error(`Failed to fetch album art: ${error.message}`);
}
}

_deleteFile(path) {
try {
const file = Gio.File.new_for_path(path);
file.delete(null);
} catch (error) {
global.logError(`Failed to delete file ${path}: ${error.message}`);
}
}

on_desklet_removed() {
if (this._timeout) {
Mainloop.source_remove(this._timeout);
}
if (this._currentArtPath) {
this._deleteFile(this._currentArtPath);
}
}
}

function main(metadata, desklet_id) {
return new LastFmDesklet(metadata, desklet_id);
}
Binary file added lastx@swud/files/lastx@swud/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions lastx@swud/files/lastx@swud/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

{
"uuid": "lastx@swud",
"name": "Last.fm Stats",
"description": "desklet that fetches and displays your currently playing or last listened track, including the song title, artist name, timestamp and album cover.",
"version": "1.0",
"max-instances": 1,
"icon": "icon.png"
}
16 changes: 16 additions & 0 deletions lastx@swud/files/lastx@swud/settings-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"lastfm_user": {
"type": "entry",
"default": "",
"description": "Last.fm Username"
},
"update_interval": {
"type": "spinbutton",
"default": 60,
"min": 30,
"max": 300,
"step": 10,
"units": "seconds",
"description": "Update Interval (seconds)"
}
}
3 changes: 3 additions & 0 deletions lastx@swud/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"author": "swudhead"
}
Binary file added lastx@swud/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.