From 959562f6b0f0d2023d444c6a367f9c3124421a3e Mon Sep 17 00:00:00 2001
From: Broque Thomas <26755000+Nezreka@users.noreply.github.com>
Date: Fri, 8 May 2026 07:31:51 -0700
Subject: [PATCH] Delete Recently Added / Top Tracks / Forgotten Favorites /
Familiar Favorites
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Owner decision: not worth shipping. The four library-driven personalized
sections were stubbed returning [] for ages because their schema
prereqs didn't exist; the prior commit re-enabled them by routing
through a new `_select_library_tracks` helper. Owner reviewed and chose
to delete the sections entirely instead.
Removed everywhere:
- `core/personalized_playlists.py` — `get_recently_added`,
`get_top_tracks`, `get_forgotten_favorites`, `get_familiar_favorites`
+ the `_select_library_tracks` helper (no other callers; verified
via grep).
- `web_server.py` — 4 route handlers
(`/api/discover/personalized/recently-added`, `top-tracks`,
`forgotten-favorites`, `familiar-favorites`).
- `webui/index.html` — 4 `
` blocks
(`#personalized-recently-added`, `#personalized-top-tracks`,
`#personalized-forgotten-favorites`,
`#personalized-familiar-favorites`).
- `webui/static/discover.js` — 4 load functions
(`loadPersonalizedRecentlyAdded`, `loadPersonalizedTopTracks`,
`loadPersonalizedForgottenFavorites`, `loadFamiliarFavorites`),
plus their entries in `loadDiscoverPage`'s Promise.all, plus
4 module-level state vars + 6 dead branches across
`openDownloadModalForDiscoverPlaylist` / `startDiscoverPlaylistSync`
and the sync-progress / rehydrate dispatchers.
- `webui/static/helper.js` — 4 tooltip / docs entries.
- `webui/static/sync-spotify.js` — 1 stale rehydrate dispatcher
branch (`discover_familiar_favorites`) caught during the global
grep pass.
- `tests/test_personalized_playlists_id_gate.py` — 3 library-method
tests + the test infrastructure that supported them
(`tracks` schema, `insert_library_track` helper). Documentation
header updated to reflect the deletion.
Net: -527 / +2 lines across 7 files.
What stays:
- Daily Mixes (also in personalized package, intentionally paused —
separate decision).
- Popular Picks + Hidden Gems + Discovery Shuffle (alive, not
affected by this deletion).
- All 14 tests in the personalized-playlists test file still pass.
- The PersonalizedPlaylistsService lift from the prior commit
(`_select_discovery_tracks` etc) — those are still in active use
by the surviving discovery_pool methods.
DISCOVER_TRACK_SELECTION_REVIEW.md at repo root contains historical
references to the four deleted endpoints. Treated as historical
context (same policy as WHATS_NEW), left alone.
2219/2219 full suite green (was 2222 - 3 deleted tests = 2219).
JS parses clean, ruff clean.
---
core/personalized_playlists.py | 142 -------------------
tests/test_personalized_playlists_id_gate.py | 94 ------------
web_server.py | 81 -----------
webui/index.html | 77 ----------
webui/static/discover.js | 115 +--------------
webui/static/helper.js | 18 +--
webui/static/sync-spotify.js | 2 -
7 files changed, 2 insertions(+), 527 deletions(-)
diff --git a/core/personalized_playlists.py b/core/personalized_playlists.py
index 9227178d..da507121 100644
--- a/core/personalized_playlists.py
+++ b/core/personalized_playlists.py
@@ -341,138 +341,6 @@ class PersonalizedPlaylistsService:
return 'Other'
- # ========================================
- # LIBRARY-BASED PLAYLISTS
- # ========================================
-
- def _select_library_tracks(
- self,
- *,
- where_clause: str = "",
- params: tuple = (),
- order_by: str = "t.created_at DESC",
- limit: int,
- ) -> List[Dict]:
- """
- Shared selector for library-based playlist methods.
-
- Builds and runs a SELECT against `tracks` joined to `albums` and
- `artists`, with a baked-in ID-validity gate so callers cannot
- accidentally return rows with no usable source IDs (which would fail
- downstream when the user clicks download or tries to match the track
- against an external metadata source).
-
- The WHERE clause always includes:
- (t.spotify_track_id IS NOT NULL
- OR t.itunes_track_id IS NOT NULL
- OR t.deezer_id IS NOT NULL
- OR t.musicbrainz_recording_id IS NOT NULL
- OR t.audiodb_id IS NOT NULL)
-
- The ID gate is mandatory and not opt-out by design — mirrors the
- shared `_select_discovery_tracks` helper.
-
- Args:
- where_clause: optional SQL fragment appended to the WHERE clause.
- Must start with "AND " if non-empty.
- params: positional bindings for `?` placeholders in `where_clause`.
- order_by: ORDER BY expression, used as-is.
- limit: LIMIT applied to the query.
-
- Returns:
- List of track dicts in the standard `_build_track_dict` shape with
- `source='library'`. Returns `[]` on any error (logged at error
- level).
- """
- try:
- query = f"""
- SELECT
- t.spotify_track_id AS spotify_track_id,
- t.itunes_track_id AS itunes_track_id,
- t.deezer_id AS deezer_track_id,
- t.title AS track_name,
- ar.name AS artist_name,
- al.title AS album_name,
- al.thumb_url AS album_cover_url,
- t.duration AS duration_ms
- FROM tracks t
- LEFT JOIN albums al ON t.album_id = al.id
- LEFT JOIN artists ar ON t.artist_id = ar.id
- WHERE (t.spotify_track_id IS NOT NULL
- OR t.itunes_track_id IS NOT NULL
- OR t.deezer_id IS NOT NULL
- OR t.musicbrainz_recording_id IS NOT NULL
- OR t.audiodb_id IS NOT NULL)
- {where_clause}
- ORDER BY {order_by}
- LIMIT ?
- """
-
- bound_params = tuple(params) + (limit,)
-
- with self.database._get_connection() as conn:
- cursor = conn.cursor()
- cursor.execute(query, bound_params)
- rows = cursor.fetchall()
-
- results: List[Dict] = []
- for row in rows:
- row_dict = dict(row) if hasattr(row, 'keys') else row
- # Library tracks have no popularity score and no JSON blob;
- # fill the standard shape with the documented defaults.
- row_dict.setdefault('popularity', 0)
- row_dict.setdefault('track_data_json', '{}')
- # Guard against NULL artist/album joins so downstream string
- # ops (diversity filter, dedupe) don't blow up.
- if row_dict.get('artist_name') is None:
- row_dict['artist_name'] = 'Unknown'
- if row_dict.get('album_name') is None:
- row_dict['album_name'] = 'Unknown'
- if row_dict.get('track_name') is None:
- row_dict['track_name'] = 'Unknown'
- if row_dict.get('duration_ms') is None:
- row_dict['duration_ms'] = 0
- results.append(self._build_track_dict(row_dict, 'library'))
-
- return results
-
- except Exception as e:
- logger.error(f"Error in _select_library_tracks: {e}")
- return []
-
- def get_recently_added(self, limit: int = 50) -> List[Dict]:
- """Get recently added tracks from the local library, newest first."""
- tracks = self._select_library_tracks(
- order_by="t.created_at DESC",
- limit=limit,
- )
- logger.info(f"Recently Added: selected {len(tracks)} library tracks")
- return tracks
-
- def get_top_tracks(self, limit: int = 50) -> List[Dict]:
- """Get the user's all-time top library tracks by play count."""
- tracks = self._select_library_tracks(
- where_clause="AND t.play_count > 0",
- order_by="t.play_count DESC",
- limit=limit,
- )
- logger.info(f"Top Tracks: selected {len(tracks)} library tracks")
- return tracks
-
- def get_forgotten_favorites(self, limit: int = 50) -> List[Dict]:
- """Get tracks the user loved (>5 plays) but hasn't played in 90+ days."""
- tracks = self._select_library_tracks(
- where_clause=(
- "AND t.play_count > 5 "
- "AND t.last_played IS NOT NULL "
- "AND t.last_played < datetime('now', '-90 days')"
- ),
- order_by="t.play_count DESC",
- limit=limit,
- )
- logger.info(f"Forgotten Favorites: selected {len(tracks)} library tracks")
- return tracks
-
def get_decade_playlist(self, decade: int, limit: int = 100, source: str = None) -> List[Dict]:
"""
Get tracks from a specific decade from discovery pool with diversity filtering.
@@ -722,16 +590,6 @@ class PersonalizedPlaylistsService:
logger.info(f"Discovery Shuffle ({active_source}): selected {len(tracks)} tracks")
return tracks
- def get_familiar_favorites(self, limit: int = 50) -> List[Dict]:
- """Get tracks with medium play counts (3-15 plays) - reliable go-tos."""
- tracks = self._select_library_tracks(
- where_clause="AND t.play_count BETWEEN 3 AND 15",
- order_by="t.play_count DESC",
- limit=limit,
- )
- logger.info(f"Familiar Favorites: selected {len(tracks)} library tracks")
- return tracks
-
# ========================================
# DAILY MIX (HYBRID PLAYLISTS)
# ========================================
diff --git a/tests/test_personalized_playlists_id_gate.py b/tests/test_personalized_playlists_id_gate.py
index ccc315a9..3f205982 100644
--- a/tests/test_personalized_playlists_id_gate.py
+++ b/tests/test_personalized_playlists_id_gate.py
@@ -10,14 +10,11 @@ section can't accidentally bypass it.
Coverage:
- `_select_discovery_tracks` filters out rows where every source ID is NULL
- `_select_discovery_tracks` honors source filter + blacklist filter
-- `_select_library_tracks` enforces the same gate against the `tracks` table
- `_apply_diversity_filter` caps per-album + per-artist counts
- `_compute_adaptive_diversity_limits` returns the right tier for the
unique-artist count + relaxed flag
- The 5 discovery_pool methods (decade / genre / popular_picks /
hidden_gems / discovery_shuffle) each filter NULL-id rows
-- The 4 library methods (recently_added / top_tracks /
- forgotten_favorites / familiar_favorites) each filter NULL-id rows
"""
from __future__ import annotations
@@ -71,31 +68,6 @@ class _FakeDatabase:
CREATE TABLE discovery_artist_blacklist (
artist_name TEXT PRIMARY KEY
);
- CREATE TABLE artists (
- id INTEGER PRIMARY KEY,
- name TEXT
- );
- CREATE TABLE albums (
- id INTEGER PRIMARY KEY,
- title TEXT,
- artist_id INTEGER,
- thumb_url TEXT
- );
- CREATE TABLE tracks (
- id INTEGER PRIMARY KEY,
- album_id INTEGER,
- artist_id INTEGER,
- title TEXT,
- duration INTEGER,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- play_count INTEGER DEFAULT 0,
- last_played TIMESTAMP,
- spotify_track_id TEXT,
- itunes_track_id TEXT,
- deezer_id TEXT,
- musicbrainz_recording_id TEXT,
- audiodb_id TEXT
- );
""")
self._conn.commit()
@@ -116,26 +88,6 @@ class _FakeDatabase:
)
self._conn.commit()
- def insert_library_track(self, *, artist_name='Library Artist',
- album_name='Library Album', track_name='Library Track',
- **track_kwargs):
- # Insert artist + album + track in one shot for convenience.
- cur = self._conn.cursor()
- cur.execute("INSERT INTO artists (name) VALUES (?)", (artist_name,))
- artist_id = cur.lastrowid
- cur.execute(
- "INSERT INTO albums (title, artist_id, thumb_url) VALUES (?, ?, ?)",
- (album_name, artist_id, 'http://x/cover.jpg'),
- )
- album_id = cur.lastrowid
- track_kwargs.setdefault('duration', 200000)
- cur.execute(
- f"""INSERT INTO tracks (album_id, artist_id, title, {', '.join(track_kwargs.keys())})
- VALUES (?, ?, ?, {', '.join(['?'] * len(track_kwargs))})""",
- (album_id, artist_id, track_name, *track_kwargs.values()),
- )
- self._conn.commit()
-
def blacklist(self, artist_name):
self._conn.execute(
"INSERT INTO discovery_artist_blacklist (artist_name) VALUES (?)",
@@ -375,49 +327,3 @@ def test_get_decade_playlist_filters_null_id_rows(service):
assert [t['track_name'] for t in out] == ['Yes']
-# ---------------------------------------------------------------------------
-# Library methods (re-enabled)
-# ---------------------------------------------------------------------------
-
-
-def test_get_recently_added_filters_null_id_rows(service):
- svc, db = service
- db.insert_library_track(
- track_name='Has Spotify', spotify_track_id='sp1',
- )
- db.insert_library_track(
- track_name='No IDs', # all source ID columns NULL
- )
- db.insert_library_track(
- track_name='Has MB', musicbrainz_recording_id='mb1',
- )
- out = svc.get_recently_added(limit=10)
- names = sorted(t['track_name'] for t in out)
- assert names == ['Has MB', 'Has Spotify']
-
-
-def test_get_top_tracks_requires_play_count(service):
- svc, db = service
- db.insert_library_track(
- track_name='Played', spotify_track_id='sp1', play_count=5,
- )
- db.insert_library_track(
- track_name='Unplayed', spotify_track_id='sp2', play_count=0,
- )
- out = svc.get_top_tracks(limit=10)
- assert [t['track_name'] for t in out] == ['Played']
-
-
-def test_get_familiar_favorites_uses_play_count_band(service):
- svc, db = service
- db.insert_library_track(
- track_name='InBand', spotify_track_id='sp1', play_count=8,
- )
- db.insert_library_track(
- track_name='TooLow', spotify_track_id='sp2', play_count=2,
- )
- db.insert_library_track(
- track_name='TooHigh', spotify_track_id='sp3', play_count=20,
- )
- out = svc.get_familiar_favorites(limit=10)
- assert [t['track_name'] for t in out] == ['InBand']
diff --git a/web_server.py b/web_server.py
index f9709dcc..dfde1998 100644
--- a/web_server.py
+++ b/web_server.py
@@ -27189,66 +27189,6 @@ def refresh_seasonal_content():
# PERSONALIZED PLAYLISTS ENDPOINTS
# ========================================
-@app.route('/api/discover/personalized/recently-added', methods=['GET'])
-def get_recently_added_playlist():
- """Get recently added tracks from library"""
- try:
- from core.personalized_playlists import get_personalized_playlists_service
-
- database = get_database()
- service = get_personalized_playlists_service(database, spotify_client)
-
- tracks = service.get_recently_added(limit=50)
-
- return jsonify({
- "success": True,
- "tracks": tracks
- })
-
- except Exception as e:
- logger.error(f"Error getting recently added playlist: {e}")
- return jsonify({"success": False, "error": str(e)}), 500
-
-@app.route('/api/discover/personalized/top-tracks', methods=['GET'])
-def get_top_tracks_playlist():
- """Get user's all-time top tracks"""
- try:
- from core.personalized_playlists import get_personalized_playlists_service
-
- database = get_database()
- service = get_personalized_playlists_service(database, spotify_client)
-
- tracks = service.get_top_tracks(limit=50)
-
- return jsonify({
- "success": True,
- "tracks": tracks
- })
-
- except Exception as e:
- logger.error(f"Error getting top tracks playlist: {e}")
- return jsonify({"success": False, "error": str(e)}), 500
-
-@app.route('/api/discover/personalized/forgotten-favorites', methods=['GET'])
-def get_forgotten_favorites_playlist():
- """Get forgotten favorites - tracks you loved but haven't played recently"""
- try:
- from core.personalized_playlists import get_personalized_playlists_service
-
- database = get_database()
- service = get_personalized_playlists_service(database, spotify_client)
-
- tracks = service.get_forgotten_favorites(limit=50)
-
- return jsonify({
- "success": True,
- "tracks": tracks
- })
-
- except Exception as e:
- logger.error(f"Error getting forgotten favorites playlist: {e}")
- return jsonify({"success": False, "error": str(e)}), 500
-
@app.route('/api/discover/personalized/decade/', methods=['GET'])
def get_decade_playlist(decade):
"""Get tracks from a specific decade"""
@@ -27353,27 +27293,6 @@ def get_discovery_shuffle():
logger.error(f"Error getting discovery shuffle playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
-@app.route('/api/discover/personalized/familiar-favorites', methods=['GET'])
-def get_familiar_favorites():
- """Get Familiar Favorites playlist - reliable go-to tracks"""
- try:
- from core.personalized_playlists import get_personalized_playlists_service
-
- database = get_database()
- service = get_personalized_playlists_service(database, spotify_client)
-
- limit = int(request.args.get('limit', 50))
- tracks = service.get_familiar_favorites(limit=limit)
-
- return jsonify({
- "success": True,
- "tracks": tracks
- })
-
- except Exception as e:
- logger.error(f"Error getting familiar favorites playlist: {e}")
- return jsonify({"success": False, "error": str(e)}), 500
-
@app.route('/api/discover/artist-blacklist', methods=['GET'])
def get_discovery_artist_blacklist():
"""Get all blacklisted discovery artists."""
diff --git a/webui/index.html b/webui/index.html
index 7b0d993a..f4159613 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -2985,17 +2985,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ⟳
- Syncing to media server...
-
-
- ✓ 0
- ⏳ 0
- ✗ 0
- (0%)
-
-
-
-
-
-
-
-