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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ TIMEOFDAY_ENABLED=true

# Time period boundaries (24-hour format)
TIMEOFDAY_MORNING_START=4
TIMEOFDAY_MORNING_END=12
TIMEOFDAY_AFTERNOON_START=12
TIMEOFDAY_MORNING_END=10
TIMEOFDAY_AFTERNOON_START=10
TIMEOFDAY_AFTERNOON_END=16
TIMEOFDAY_EVENING_START=16
TIMEOFDAY_EVENING_END=22
Expand Down
2 changes: 1 addition & 1 deletion ENV_VARS.md
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ PERF_SCAN_TIMEOUT=60

### PERF_DOWNLOAD_DELAY
**Description**: Delay between downloads (seconds)
**Default**: `6`
**Default**: `10`
**Range**: `1` to `30`
**Example**:
```bash
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ services:
# ============================================================================
# Cron expression for automatic scheduling (e.g., "0 2 * * *" for daily at 2 AM)
# Leave unset or use "manual" to disable scheduling
SCHEDULE_CRON: ${SCHEDULE_CRON:-0 2,6,12,16,22 * * *}
SCHEDULE_CRON: ${SCHEDULE_CRON:-0 2,4,10,16,22 * * *}

# Timezone for scheduled runs (IANA timezone name)
TZ: ${TZ:-America/Chicago}
Expand Down Expand Up @@ -132,8 +132,8 @@ services:

# Time period boundaries (24-hour format)
TIMEOFDAY_MORNING_START: ${TIMEOFDAY_MORNING_START:-4}
TIMEOFDAY_MORNING_END: ${TIMEOFDAY_MORNING_END:-12}
TIMEOFDAY_AFTERNOON_START: ${TIMEOFDAY_AFTERNOON_START:-12}
TIMEOFDAY_MORNING_END: ${TIMEOFDAY_MORNING_END:-10}
TIMEOFDAY_AFTERNOON_START: ${TIMEOFDAY_AFTERNOON_START:-10}
TIMEOFDAY_AFTERNOON_END: ${TIMEOFDAY_AFTERNOON_END:-16}
TIMEOFDAY_EVENING_START: ${TIMEOFDAY_EVENING_START:-16}
TIMEOFDAY_EVENING_END: ${TIMEOFDAY_EVENING_END:-22}
Expand Down
14 changes: 14 additions & 0 deletions octogen/ai/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,20 @@ def generate_all_playlists(
]
all_playlists[playlist_name] = valid_songs

EXPECTED_PLAYLISTS = {
"Discovery", "Daily Mix 1", "Daily Mix 2", "Daily Mix 3",
"Daily Mix 4", "Daily Mix 5", "Daily Mix 6",
"Chill Vibes", "Workout Energy", "Focus Flow", "Drive Time"
}

# Filter out any hallucinated extra playlists
unexpected = [k for k in all_playlists if k not in EXPECTED_PLAYLISTS]
if unexpected:
logger.warning("AI returned unexpected playlists (filtered): %s", unexpected)
for k in unexpected:
del all_playlists[k]


self.response_cache = all_playlists
self._record_ai_call()
total = sum(len(songs) for songs in all_playlists.values())
Expand Down
8 changes: 7 additions & 1 deletion octogen/api/lastfm.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,18 @@ def get_recommended_tracks(self, limit: int = 50) -> List[Dict]:
continue

for track in tracks_response["toptracks"].get("track", []):
raw_mbid = track.get("mbid", "")
track_mbid = raw_mbid if raw_mbid else None

recommendations.append({
"artist": track["artist"]["name"],
"title": track["name"]
"title": track["name"],
"mbid": track_mbid,
})

if len(recommendations) >= limit:
break

if len(recommendations) >= limit:
break

Expand Down
11 changes: 8 additions & 3 deletions octogen/api/listenbrainz.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,14 @@ def get_created_for_you_playlists(self, count: int = 25, offset: int = 0) -> Lis

playlists = response["playlists"]

# Add minimal validation: only include playlists with 'id' and 'name'
required = {"id", "name"}
playlists = [p for p in playlists if all(k in p for k in required)]
# Validate: the ListenBrainz API returns JSPF format where each item is
# {"playlist": {"title": ..., "identifier": ..., "track": [...], ...}}
# The old filter checked for top-level 'id' and 'name' keys which don't
# exist in JSPF format, causing ALL playlists to be silently dropped.
playlists = [
p for p in playlists
if p.get("playlist", {}).get("title") and p.get("playlist", {}).get("identifier")
]

# DEBUG
if playlists:
Expand Down
22 changes: 19 additions & 3 deletions octogen/api/navidrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ def _calculate_match_score(self, search_artist: str, search_title: str,
title_ratio = difflib.SequenceMatcher(None, search_title, result_title).ratio()
return (artist_ratio * 0.5) + (title_ratio * 0.5)

def search_song(self, artist: str, title: str) -> Optional[str]:
def search_song(self, artist: str, title: str, mbid: str = None) -> Optional[str]:
"""Search for a song with fuzzy matching and version detection.

Args:
Expand All @@ -410,8 +410,24 @@ def search_song(self, artist: str, title: str) -> Optional[str]:
Returns:
Song ID if found, None otherwise
"""

# Normalize search terms
if mbid:
# Try a targeted artist+title search first, then validate MBID on the results
mbid_check_response = self._request("search3", {
"query": f'"{artist}" "{title}"',
"songCount": 10,
"artistCount": 0,
"albumCount": 0
})
if mbid_check_response:
for song in mbid_check_response.get("searchResult3", {}).get("song", []):
if song.get("musicBrainzId") == mbid:
logger.debug("MBID exact match: %s - %s",
song.get("artist"), song.get("title"))
return song["id"]
logger.debug("MBID lookup missed for %s, falling through to fuzzy", mbid)


# Step 1: Normalize search terms
search_artist_norm = self._normalize_for_comparison(artist, preserve_version=False)
search_title_norm = self._normalize_for_comparison(title, preserve_version=False)
search_version = self._has_version_marker(title)
Expand Down
2 changes: 1 addition & 1 deletion octogen/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def load_config_from_env() -> Dict:
"max_albums_scan": int(os.getenv("MAX_ALBUMS_SCAN", "10000")),
"scan_timeout": int(os.getenv("SCAN_TIMEOUT", "60")),
"download_delay_seconds": int(os.getenv("DOWNLOAD_DELAY_SECONDS", "10")),
"post_scan_delay_seconds": int(os.getenv("POST_SCAN_DELAY_SECONDS", "3")),
"post_scan_delay_seconds": int(os.getenv("POST_SCAN_DELAY_SECONDS", "30")),
"download_batch_size": int(os.getenv("DOWNLOAD_BATCH_SIZE", "5")),
"download_concurrency": int(os.getenv("DOWNLOAD_CONCURRENCY", "3")),
},
Expand Down
Loading
Loading