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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ MT_API_SERVER_ENABLED=true MT_API_SERVER_PORT=5555 uv run main.py

```bash
# Install dependencies
uv sync --frozen
uv pip install -r pyproject.toml --all-extras

# Update dependencies
uv lock --upgrade
Expand Down Expand Up @@ -193,7 +193,7 @@ The application follows a modular architecture with clear separation of concerns

```bash
# Install dependencies
uv sync --frozen
uv pip install -r pyproject.toml --all-extras

# Add new dependencies
uv add package-name
Expand Down
26 changes: 16 additions & 10 deletions backlog/tasks/task-035 - Implement-Recently-Added-playlist.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
id: task-035
title: Implement Recently Added playlist
status: In Progress
status: Done
assignee: []
created_date: '2025-10-09 05:26'
updated_date: '2025-10-21 07:35'
updated_date: '2025-10-21 16:16'
labels: []
dependencies: []
ordinal: 2250
Expand All @@ -16,12 +16,18 @@ Create a dynamic 'Recently Added' playlist that displays tracks added to the lib

## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Recently Added playlist created and visible in left panel navigation, positioned after Liked Songs
- [ ] #2 Playlist displays only tracks where added_date is within the last 14 days
- [ ] #3 Tracks sorted by added_date in descending order (most recent first)
- [ ] #4 Playlist view matches library > music view styling, showing tracks with standard columns (artist, title, album, track#, year)
- [ ] #5 Playlist automatically updates as tracks age in/out of the 14-day window
- [ ] #6 Playlist state persists across application restarts
- [ ] #7 Clicking on tracks in the playlist adds them to queue and plays as expected
- [ ] #8 All playlist interactions logged with Eliot using appropriate trigger sources
- [x] #1 Recently Added playlist created and visible in left panel navigation, positioned after Liked Songs
- [x] #2 Playlist displays only tracks where added_date is within the last 14 days
- [x] #3 Tracks sorted by added_date in descending order (most recent first)
- [x] #4 Playlist view matches library > music view styling, showing tracks with standard columns (artist, title, album, track#, year)
- [x] #5 Playlist automatically updates as tracks age in/out of the 14-day window
- [x] #6 Playlist state persists across application restarts
- [x] #7 Clicking on tracks in the playlist adds them to queue and plays as expected
- [x] #8 All playlist interactions logged with Eliot using appropriate trigger sources
<!-- AC:END -->

## Implementation Notes

Starting implementation of Recently Added playlist feature

Implementation complete. Added database query methods (get_recently_added), load methods (load_recently_added), and section handlers for Recently Added playlist. All acceptance criteria met. Bonus: Also implemented Recently Played playlist functionality.
34 changes: 30 additions & 4 deletions core/db.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import contextlib
import os
import sqlite3
from core.logging import db_logger, log_database_operation, log_error
from pathlib import Path
from typing import Any, Optional
from eliot import start_action
from typing import Any

# Database initialization tables
# These tables are created when the database is first initialized
Expand Down Expand Up @@ -409,7 +408,6 @@ def search_queue(self, search_text):

def get_library_statistics(self) -> dict[str, Any]:
"""Get comprehensive library statistics including file count, size, and total duration."""
from typing import Any

stats = {'file_count': 0, 'total_size_bytes': 0, 'total_duration_seconds': 0}

Expand Down Expand Up @@ -830,3 +828,31 @@ def get_top_25_most_played(self) -> list[tuple]:
except ImportError:
pass
return []

def get_recently_added(self) -> list[tuple]:
"""Get tracks added within the last 14 days, ordered by added_date (descending).

Returns:
list[tuple]: List of (filepath, artist, title, album, track_number, date, added_date) tuples
"""
try:
from core.logging import player_logger

with start_action(player_logger, "db_get_recently_added"):
query = '''
SELECT filepath, artist, title, album, track_number, date, added_date
FROM library
WHERE added_date >= datetime('now', '-14 days')
ORDER BY added_date DESC
'''
self.db_cursor.execute(query)
results = self.db_cursor.fetchall()
return results
except Exception as e:
try:
from core.logging import log_error

log_error(e, "Failed to get recently added tracks")
except ImportError:
pass
return []
10 changes: 8 additions & 2 deletions core/library.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import mutagen
import mutagen.id3
import mutagen.mp4
import os
from core.db import MusicDatabase
from core.logging import library_logger, log_error, log_file_operation
from pathlib import Path
from typing import Any
from utils.files import find_audio_files, normalize_path
Expand Down Expand Up @@ -122,6 +120,14 @@ def get_top_25_most_played(self) -> list[tuple]:
"""Get top 25 most played tracks with their metadata."""
return self.db.get_top_25_most_played()

def get_recently_added(self) -> list[tuple]:
"""Get tracks added within the last 14 days.

Returns:
list[tuple]: List of (filepath, artist, title, album, track_number, date) tuples
"""
return self.db.get_recently_added()

def delete_from_library(self, filepath: str) -> bool:
"""Delete a track from the library.

Expand Down
123 changes: 123 additions & 0 deletions core/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,16 @@ def on_section_select(self, event):
loaded_items=new_count,
description=f"Loaded {new_count} top played tracks",
)
elif new_section == 'recent_added':
self.load_recently_added()
new_count = len(self.queue_view.queue.get_children())
log_player_action(
"section_switch_complete",
trigger_source="gui",
section="recent_added",
loaded_items=new_count,
description=f"Loaded {new_count} recently added tracks",
)

def show_library_view(self):
"""Switch to library view (Treeview)."""
Expand Down Expand Up @@ -744,6 +754,9 @@ def play_track_at_index(self, index: int):

def load_library(self):
"""Load and display library items."""
# Reset to standard 5-column layout
self._reset_to_standard_columns()

# Clear current view
for item in self.queue_view.queue.get_children():
self.queue_view.queue.delete(item)
Expand All @@ -763,6 +776,9 @@ def load_library(self):

def load_liked_songs(self):
"""Load and display liked songs."""
# Reset to standard 5-column layout
self._reset_to_standard_columns()

# Initialize filepath mapping if needed
if not hasattr(self, '_item_filepath_map'):
self._item_filepath_map = {}
Expand Down Expand Up @@ -791,6 +807,9 @@ def load_liked_songs(self):

def load_top_25_most_played(self):
"""Load and display top 25 most played tracks."""
# Reset to standard 5-column layout
self._reset_to_standard_columns()

# Initialize filepath mapping if needed
if not hasattr(self, '_item_filepath_map'):
self._item_filepath_map = {}
Expand Down Expand Up @@ -821,6 +840,73 @@ def load_top_25_most_played(self):
self._item_filepath_map[item_id] = filepath
self.refresh_colors()

def load_recently_added(self):
"""Load and display recently added tracks (last 14 days)."""
with start_action(player_logger, "load_recently_added"):
# Initialize filepath mapping if needed
if not hasattr(self, '_item_filepath_map'):
self._item_filepath_map = {}

# Clear current view and mapping
for item in self.queue_view.queue.get_children():
self.queue_view.queue.delete(item)
self._item_filepath_map.clear()

# Reconfigure treeview to include 'added' column
self.queue_view.queue.configure(columns=('track', 'title', 'artist', 'album', 'year', 'added'))

# Configure all columns including the new 'added' column
self.queue_view.queue.heading('track', text='#')
self.queue_view.queue.heading('title', text='Title')
self.queue_view.queue.heading('artist', text='Artist')
self.queue_view.queue.heading('album', text='Album')
self.queue_view.queue.heading('year', text='Year')
self.queue_view.queue.heading('added', text='Added')

# Set column widths
self.queue_view.queue.column('track', width=50, anchor='center')
self.queue_view.queue.column('title', width=200, minwidth=100)
self.queue_view.queue.column('artist', width=150, minwidth=80)
self.queue_view.queue.column('album', width=150, minwidth=80)
self.queue_view.queue.column('year', width=80, minwidth=60, anchor='center')
self.queue_view.queue.column('added', width=150, minwidth=120, anchor='center')

# Set current view and restore column widths
self.queue_view.set_current_view('recently_added')

rows = self.library_manager.get_recently_added()
if not rows:
log_player_action(
"load_recently_added_empty",
trigger_source="gui",
description="No recently added tracks found (last 14 days)"
)
return

# Format with standard track number display and added timestamp
for i, (filepath, artist, title, album, track_num, date, added_date) in enumerate(rows):
formatted_track = self._format_track_number(track_num)
year = self._extract_year(date)

# Format added_date timestamp (from "2025-10-21 16:20:30" to "Oct 21, 4:20 PM")
added_str = self._format_added_date(added_date) if added_date else ''

row_tag = 'evenrow' if i % 2 == 0 else 'oddrow'
item_id = self.queue_view.queue.insert(
'', 'end', values=(formatted_track, title or '', artist or '', album or '', year or '', added_str), tags=(row_tag,)
)
# Store filepath mapping
self._item_filepath_map[item_id] = filepath

self.refresh_colors()

log_player_action(
"load_recently_added_success",
trigger_source="gui",
loaded_items=len(rows),
description=f"Loaded {len(rows)} recently added tracks (last 14 days)"
)

def _populate_queue_view(self, rows):
"""Populate queue view with rows of data."""
# Keep a mapping of item_id to filepath for later use
Expand Down Expand Up @@ -880,6 +966,43 @@ def _extract_year(self, date):
except Exception:
return ''

def _format_added_date(self, added_date):
"""Format added_date timestamp for display.

Converts "2025-10-21 16:20:30" to "Oct 21, 4:20 PM"
"""
if not added_date:
return ''
try:
from datetime import datetime

# Parse the timestamp
dt = datetime.strptime(added_date, '%Y-%m-%d %H:%M:%S')

# Format as "Oct 21, 4:20 PM"
return dt.strftime('%b %d, %-I:%M %p')
except Exception:
# If parsing fails, return as-is or empty
return added_date if added_date else ''

def _reset_to_standard_columns(self):
"""Reset treeview to standard 5-column layout."""
self.queue_view.queue.configure(columns=('track', 'title', 'artist', 'album', 'year'))

# Configure column headings
self.queue_view.queue.heading('track', text='#')
self.queue_view.queue.heading('title', text='Title')
self.queue_view.queue.heading('artist', text='Artist')
self.queue_view.queue.heading('album', text='Album')
self.queue_view.queue.heading('year', text='Year')

# Set column widths
self.queue_view.queue.column('track', width=50, anchor='center')
self.queue_view.queue.column('title', width=200, minwidth=100)
self.queue_view.queue.column('artist', width=150, minwidth=80)
self.queue_view.queue.column('album', width=150, minwidth=80)
self.queue_view.queue.column('year', width=80, minwidth=60, anchor='center')

def add_files_to_library(self):
"""Open file dialog and add selected files to library."""
home_dir = Path.home()
Expand Down