Skip to content
Merged
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
4 changes: 2 additions & 2 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -478,10 +478,10 @@
"plugin_path": "plugins/ledmatrix-music",
"stars": 0,
"downloads": 0,
"last_updated": "2026-05-15",
"last_updated": "2026-07-09",
"verified": true,
"screenshot": "",
"latest_version": "1.0.6"
"latest_version": "1.0.8"
},
{
"id": "news",
Expand Down
30 changes: 13 additions & 17 deletions plugins/ledmatrix-music/LICENSE
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
MIT License
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007

Copyright (c) 2025 ChuckBuilds
Copyright (C) 2025 LEDMatrix Team

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
2 changes: 1 addition & 1 deletion plugins/ledmatrix-music/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ For issues, feature requests, or questions:

## License

This plugin is licensed under the MIT License. See the LICENSE file for details.
This plugin is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.

## Contributing

Expand Down
74 changes: 60 additions & 14 deletions plugins/ledmatrix-music/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,51 @@ def update(self) -> None:
if not self.poll_thread or not self.poll_thread.is_alive():
self.start_polling()

def _clip_text_to_width(self, text, font, max_width):
"""Trim trailing characters so the rendered text fits within max_width px.

The scrolling marquee rotates which characters lead, but the drawn
string is still full-length; without this clip it overflows the right
edge of the panel. This keeps the text inside its viewport, which
matters most on narrow 64px-wide displays where the text area is only
~29px wide.
"""
if max_width <= 0:
# No horizontal room for text (e.g. a square panel where the album
# art fills the full width) — draw nothing rather than overflow.
return ""
if not text:
return text
if self.display_manager.get_text_width(text, font) <= max_width:
return text
clipped = text
while clipped and self.display_manager.get_text_width(clipped, font) > max_width:
clipped = clipped[:-1]
return clipped

def _truncate_text_with_ellipsis(self, text, font, max_width):
"""Truncate text to fit max_width px, appending "..." when trimmed.

Used by the scrolling-disabled branches. Reserves room for the
ellipsis before clipping, so the result already fits the viewport and
survives the draw-time _clip_text_to_width pass unchanged — otherwise
that pass would trim the trailing "..." back off and drop the
truncation indicator.
"""
if max_width <= 0:
return ""
if not text:
return text
if self.display_manager.get_text_width(text, font) <= max_width:
return text
ellipsis = "..."
ellipsis_width = self.display_manager.get_text_width(ellipsis, font)
if ellipsis_width >= max_width:
# Not even room for the ellipsis — clip the raw text instead.
return self._clip_text_to_width(text, font, max_width)
clipped = self._clip_text_to_width(text, font, max_width - ellipsis_width)
return clipped + ellipsis

def display(self, force_clear: bool = False) -> None:
"""Display music information - called by plugin system."""
perform_full_refresh_this_cycle = force_clear
Expand Down Expand Up @@ -825,10 +870,11 @@ def display(self, force_clear: bool = False) -> None:
if perform_full_refresh_this_cycle or not self.is_currently_showing_nothing_playing:
self.display_manager.clear()

text_width = self.display_manager.get_text_width("Nothing Playing", self.display_manager.regular_font)
x_pos = (self.display_manager.matrix.width - text_width) // 2
np_text = self._clip_text_to_width("Nothing Playing", self.display_manager.regular_font, self.display_manager.matrix.width)
text_width = self.display_manager.get_text_width(np_text, self.display_manager.regular_font)
x_pos = max(0, (self.display_manager.matrix.width - text_width) // 2)
y_pos = (self.display_manager.matrix.height // 2) - 4
self.display_manager.draw_text("Nothing Playing", x=x_pos, y=y_pos, font=self.display_manager.regular_font)
self.display_manager.draw_text(np_text, x=x_pos, y=y_pos, font=self.display_manager.regular_font)
self.display_manager.update_display()
self.is_currently_showing_nothing_playing = True

Expand Down Expand Up @@ -1044,8 +1090,8 @@ def _safe_y_percent(value, fallback):
self.scroll_position_title = (self.scroll_position_title + 1) % len(title)
self.title_scroll_tick = 0
elif title_width > text_area_width and not title_config['enabled']:
# Scrolling disabled - truncate text
current_title_display_text = title[:text_area_width // 6] + "..." # Rough truncation
# Scrolling disabled - truncate to the viewport with an ellipsis
current_title_display_text = self._truncate_text_with_ellipsis(title, font_title, text_area_width)
self.scroll_position_title = 0
self.title_scroll_tick = 0
else:
Expand All @@ -1056,7 +1102,7 @@ def _safe_y_percent(value, fallback):
self.title_end_pause_counter = 0
self.title_at_end = False

self.display_manager.draw_text(current_title_display_text,
self.display_manager.draw_text(self._clip_text_to_width(current_title_display_text, font_title, text_area_width),
x=text_area_x_start, y=y_pos_title_top, color=(255, 255, 255), font=font_title)

# Artist scrolling with configurable settings
Expand Down Expand Up @@ -1099,8 +1145,8 @@ def _safe_y_percent(value, fallback):
self.scroll_position_artist = (self.scroll_position_artist + 1) % len(artist)
self.artist_scroll_tick = 0
elif artist_width > text_area_width and not artist_config['enabled']:
# Scrolling disabled - truncate text
current_artist_display_text = artist[:text_area_width // 5] + "..." # Rough truncation
# Scrolling disabled - truncate to the viewport with an ellipsis
current_artist_display_text = self._truncate_text_with_ellipsis(artist, font_artist, text_area_width)
self.scroll_position_artist = 0
self.artist_scroll_tick = 0
else:
Expand All @@ -1111,7 +1157,7 @@ def _safe_y_percent(value, fallback):
self.artist_end_pause_counter = 0
self.artist_at_end = False

self.display_manager.draw_text(current_artist_display_text,
self.display_manager.draw_text(self._clip_text_to_width(current_artist_display_text, font_artist, text_area_width),
x=text_area_x_start, y=y_pos_artist_top, color=(180, 180, 180), font=font_artist)

# Album
Expand All @@ -1127,7 +1173,7 @@ def _safe_y_percent(value, fallback):
if album_width <= text_area_width:
# Album fits without scrolling - display normally
self.logger.debug(f"MusicPlugin.display: Drawing album '{album}' at ({text_area_x_start}, {y_pos_album_top}) - fits without scrolling")
self.display_manager.draw_text(album,
self.display_manager.draw_text(self._clip_text_to_width(album, font_album, text_area_width),
x=text_area_x_start, y=y_pos_album_top, color=(150, 150, 150), font=font_album)
self.scroll_position_album = 0
self.album_scroll_tick = 0
Expand Down Expand Up @@ -1173,13 +1219,13 @@ def _safe_y_percent(value, fallback):
self.scroll_position_album = (self.scroll_position_album + 1) % len(album)
self.album_scroll_tick = 0
else:
# Scrolling disabled - truncate text
current_album_display_text = album[:text_area_width // 5] + "..." # Rough truncation
# Scrolling disabled - truncate to the viewport with an ellipsis
current_album_display_text = self._truncate_text_with_ellipsis(album, font_album, text_area_width)
self.scroll_position_album = 0
self.album_scroll_tick = 0

self.logger.debug(f"MusicPlugin.display: Drawing scrolling album '{current_album_display_text}' at ({text_area_x_start}, {y_pos_album_top}) - position: {self.scroll_position_album}")
self.display_manager.draw_text(current_album_display_text,
self.display_manager.draw_text(self._clip_text_to_width(current_album_display_text, font_album, text_area_width),
x=text_area_x_start, y=y_pos_album_top, color=(150, 150, 150), font=font_album)
else:
self.logger.debug(f"MusicPlugin.display: Album '{album}' not displayed - insufficient height (available: {available_height_for_album}, needed: {album_height})")
Expand All @@ -1188,7 +1234,7 @@ def _safe_y_percent(value, fallback):
duration_ms = current_track_info_snapshot.get('duration_ms', 0)
progress_ms = current_track_info_snapshot.get('progress_ms', 0)

if duration_ms > 0:
if duration_ms > 0 and text_area_width > 0:
bar_total_width = text_area_width
filled_ratio = progress_ms / duration_ms
filled_width = int(filled_ratio * bar_total_width)
Expand Down
20 changes: 16 additions & 4 deletions plugins/ledmatrix-music/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "ledmatrix-music",
"name": "Music Player - Now Playing",
"version": "1.0.6",
"version": "1.0.8",
"description": "Real-time now playing display for Spotify and YouTube Music with album art, scrolling text, and progress bars",
"author": "ChuckBuilds",
"entry_point": "manager.py",
Expand All @@ -28,7 +28,7 @@
],
"homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/ledmatrix-music",
"website": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/ledmatrix-music",
"license": "MIT",
"license": "GPL-3.0",
"compatible_versions": [
">=2.0.0"
],
Expand Down Expand Up @@ -66,10 +66,22 @@
}
],
"versions": [
{
"released": "2026-07-09",
"version": "1.0.8",
"notes": "When text scrolling is disabled, truncate the title/artist/album to the text-area width and append an ellipsis, reserving room for it so the '...' indicator isn't clipped away at draw time.",
"ledmatrix_min_version": "2.0.0"
},
{
"released": "2026-07-08",
"version": "1.0.7",
"notes": "Fix now-playing text overflowing the right edge on narrow (64px-wide) panels: clip the scrolling title/artist/album text to the text-area viewport so long text stays within bounds while it scrolls, instead of drawing past the panel edge. Also set the license to GPL-3.0 (project standard).",
"ledmatrix_min_version": "2.0.0"
},
{
"released": "2026-05-15",
"version": "1.0.6",
"ledmatrix_min": "2.0.0"
"ledmatrix_min_version": "2.0.0"
},
{
"version": "1.0.5",
Expand All @@ -92,7 +104,7 @@
"ledmatrix_min_version": "2.0.0"
}
],
"last_updated": "2026-05-15",
"last_updated": "2026-07-09",
"stars": 0,
"downloads": 0,
"verified": true,
Expand Down
Loading