This commit is contained in:
Broque Thomas 2025-07-28 18:59:19 -07:00
parent 6e5a0f76f6
commit fc87fe1740
7 changed files with 4137 additions and 16 deletions

View file

@ -1 +1 @@
{"access_token": "BQCAxqYls1GZziWWT_RK2oephXS0gdOA6aCR_ylemRRdjvZ3ZAvgzQDiZcOvP6DdENbfoZK2d5m-a61GUp1syw9sMS6U3qjvjhGGgACvl4jytuArO-ht2crjJmeVdl_88YIrBMrlZsBnOAmOyPH8xA7X1i9hy5H5_nO32iiM1t96sm9rT9zIAe6gCQpDt36SbLafT2GZz_7dH3k6QVZRkuESnyr9xXznIsshhemd2UFnRnTGzg8ptWp7eCVAiL5d", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753746945, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
{"access_token": "BQAX2tP36P49Pqyfkqzyd5Flp_POENiQdd91NLRXhAnH2Ha8UyiAkM8PkubgHOUbZ3qWDtrPAatNMzHwhxwA4fSCBiRT9Aq8CMq4ayGGANhZxHC04XrQMan7oKKyeHvx6P46DrqDwGAeGx8joR2rUswd0C-iMpSqK8nk3krWdCUVhabMuDxVFtRWsTqbdmnXVNsqh9QK-XABRE0PZ2U1ZTaPJwT1M3gFvYKHxj-Rym3qbHR5kqSuEohZXtcKZBMq", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753756658, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}

View file

@ -6,6 +6,8 @@ from plexapi.exceptions import PlexApiException, NotFound
from typing import List, Optional, Dict, Any
from dataclasses import dataclass
import requests
from datetime import datetime, timedelta
import re
from utils.logging_config import get_logger
from config.settings import config_manager
@ -439,7 +441,11 @@ class PlexClient:
for genre in genres:
artist.addGenre(genre)
logger.info(f"Updated genres for {artist.title}: {genres}")
# Use safe logging to avoid Unicode encoding errors
try:
logger.info(f"Updated genres for {artist.title}: {len(genres)} genres")
except UnicodeEncodeError:
logger.info(f"Updated genres for artist (ID: {artist.ratingKey}): {len(genres)} genres")
return True
except Exception as e:
logger.error(f"Error updating genres for {artist.title}: {e}")
@ -467,6 +473,123 @@ class PlexClient:
logger.error(f"Error updating poster for {artist.title}: {e}")
return False
def parse_update_timestamp(self, artist: PlexArtist) -> Optional[datetime]:
"""Parse the last update timestamp from artist summary"""
try:
# Get artist summary which stores our timestamp
summary = getattr(artist, 'summary', '') or ''
# Look for timestamp pattern: -updatedAtYYYY-MM-DD
pattern = r'-updatedAt(\d{4}-\d{2}-\d{2})'
match = re.search(pattern, summary)
if match:
date_str = match.group(1)
return datetime.strptime(date_str, '%Y-%m-%d')
return None
except Exception as e:
logger.debug(f"Error parsing timestamp for {artist.title}: {e}")
return None
def is_artist_ignored(self, artist: PlexArtist) -> bool:
"""Check if artist is manually marked to be ignored"""
try:
# Check summary field where we store timestamps and ignore flags
summary = getattr(artist, 'summary', '') or ''
return '-IgnoreUpdate' in summary
except Exception as e:
logger.debug(f"Error checking ignore status for {artist.title}: {e}")
return False
def needs_update_by_age(self, artist: PlexArtist, refresh_interval_days: int) -> bool:
"""Check if artist needs updating based on age threshold"""
try:
# First check if artist is manually ignored
if self.is_artist_ignored(artist):
logger.debug(f"Artist {artist.title} is manually ignored")
return False
# If refresh_interval_days is 0, always update (full refresh)
if refresh_interval_days == 0:
return True
last_update = self.parse_update_timestamp(artist)
# If no timestamp found, needs update
if last_update is None:
return True
# Check if last update is older than threshold
threshold_date = datetime.now() - timedelta(days=refresh_interval_days)
return last_update < threshold_date
except Exception as e:
logger.debug(f"Error checking update age for {artist.title}: {e}")
return True # Default to needing update if error
def update_artist_biography(self, artist: PlexArtist) -> bool:
"""Update artist summary with current timestamp"""
try:
# Get current summary/biography
current_summary = getattr(artist, 'summary', '') or ''
print(f"[DEBUG] Original summary for '{artist.title}': '{current_summary[:100]}...'")
# Preserve any IgnoreUpdate flag
ignore_flag = ''
if '-IgnoreUpdate' in current_summary:
ignore_flag = '-IgnoreUpdate'
# Remove IgnoreUpdate flag temporarily for processing
current_summary = current_summary.replace('-IgnoreUpdate', '').strip()
# Remove existing timestamp if present (ensures only one timestamp)
pattern = r'\s*-updatedAt\d{4}-\d{2}-\d{2}\s*'
clean_summary = re.sub(pattern, '', current_summary).strip()
# Build new summary with timestamp
today = datetime.now().strftime('%Y-%m-%d')
# Add timestamp to summary field
new_summary = clean_summary
if ignore_flag:
new_summary = f"{new_summary}\n\n{ignore_flag}".strip()
new_summary = f"{new_summary}\n\n-updatedAt{today}".strip()
print(f"[DEBUG] Setting summary for '{artist.title}': ending with '{new_summary[-50:]}'")
# Use the correct Plex API syntax with .value (testing without lock first)
artist.edit(**{
'summary.value': new_summary
})
print(f"[DEBUG] Called artist.edit with summary.value (no lock) for '{artist.title}'")
# Add a small delay to let the edit process
import time
time.sleep(0.5)
# Reload to see the changes
artist.reload()
print(f"[DEBUG] Called artist.reload() for '{artist.title}'")
# Check if edit worked
updated_summary = getattr(artist, 'summary', '') or ''
print(f"[DEBUG] Summary after reload for '{artist.title}': ending with '{updated_summary[-50:]}'")
if updated_summary and '-updatedAt' in updated_summary:
logger.info(f"Updated summary timestamp for {artist.title}")
return True
else:
print(f"[DEBUG] Timestamp not found in summary after reload for '{artist.title}'")
return False
except Exception as e:
print(f"[DEBUG] Exception in update_artist_biography for '{artist.title}': {e}")
logger.error(f"Error updating summary for {artist.title}: {e}")
import traceback
traceback.print_exc()
return False
def update_track_metadata(self, track_id: str, metadata: Dict[str, Any]) -> bool:
if not self.ensure_connection():
return False

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QFrame, QGridLayout, QScrollArea, QSizePolicy, QPushButton,
QProgressBar, QTextEdit, QSpacerItem, QGroupBox, QFormLayout)
QProgressBar, QTextEdit, QSpacerItem, QGroupBox, QFormLayout, QComboBox)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject
from PyQt6.QtGui import QFont, QPalette, QColor
import time
@ -29,12 +29,13 @@ class MetadataUpdateWorker(QThread):
error = pyqtSignal(str) # error_message
artists_loaded = pyqtSignal(int, int) # total_artists, artists_to_process
def __init__(self, artists, plex_client, spotify_client):
def __init__(self, artists, plex_client, spotify_client, refresh_interval_days=30):
super().__init__()
self.artists = artists
self.plex_client = plex_client
self.spotify_client = spotify_client
self.matching_engine = MusicMatchingEngine()
self.refresh_interval_days = refresh_interval_days
self.should_stop = False
self.processed_count = 0
self.successful_count = 0
@ -116,18 +117,11 @@ class MetadataUpdateWorker(QThread):
self.error.emit(f"Metadata update failed: {str(e)}")
def artist_needs_processing(self, artist):
"""Check if an artist needs metadata processing using smart detection"""
"""Check if an artist needs metadata processing using age-based detection"""
try:
# Check if artist has a valid photo
has_valid_photo = self.artist_has_valid_photo(artist)
# Check if artist has genres (more than just basic ones)
existing_genres = set(genre.tag if hasattr(genre, 'tag') else str(genre)
for genre in (artist.genres or []))
has_good_genres = len(existing_genres) >= 2 # At least 2 genres indicates Spotify processing
# Process if missing photo OR insufficient genres
return not has_valid_photo or not has_good_genres
# Use PlexClient's age-based checking with configured interval
# This also handles the ignore flag check internally
return self.plex_client.needs_update_by_age(artist, self.refresh_interval_days)
except Exception as e:
print(f"Error checking artist {getattr(artist, 'title', 'Unknown')}: {e}")
@ -178,9 +172,16 @@ class MetadataUpdateWorker(QThread):
changes_made.append("genres")
if changes_made:
# Update artist biography with timestamp to track last update
biography_updated = self.plex_client.update_artist_biography(artist)
if biography_updated:
changes_made.append("timestamp")
details = f"Updated {', '.join(changes_made)} (match: '{spotify_artist.name}', score: {highest_score:.2f})"
return True, details
else:
# Even if no metadata changes, update biography to record we checked this artist
self.plex_client.update_artist_biography(artist)
return True, "Already up to date"
except Exception as e:
@ -774,6 +775,11 @@ class MetadataUpdaterWidget(QFrame):
header_label.setFont(QFont("Arial", 14, QFont.Weight.Bold))
header_label.setStyleSheet("color: #ffffff;")
# Info label
info_label = QLabel("(type -IgnoreUpdate into artist summary to ignore metadata updates on this artist)")
info_label.setFont(QFont("Arial", 9))
info_label.setStyleSheet("color: #b3b3b3; margin-bottom: 5px;")
# Control section
control_layout = QHBoxLayout()
control_layout.setSpacing(15)
@ -801,6 +807,59 @@ class MetadataUpdaterWidget(QFrame):
}
""")
# Refresh interval dropdown
refresh_info_layout = QVBoxLayout()
refresh_label = QLabel("Refresh Interval:")
refresh_label.setFont(QFont("Arial", 9))
refresh_label.setStyleSheet("color: #b3b3b3;")
self.refresh_interval_combo = QComboBox()
self.refresh_interval_combo.setFixedHeight(32)
self.refresh_interval_combo.setFont(QFont("Arial", 10))
self.refresh_interval_combo.addItems([
"6 months",
"3 months",
"1 month",
"2 weeks",
"1 week",
"Full refresh"
])
self.refresh_interval_combo.setCurrentText("1 month") # Default selection
self.refresh_interval_combo.setStyleSheet("""
QComboBox {
background: #333333;
color: #ffffff;
border: 1px solid #555555;
border-radius: 4px;
padding: 4px 8px;
min-width: 100px;
}
QComboBox:hover {
border: 1px solid #1db954;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #ffffff;
margin-right: 5px;
}
QComboBox QAbstractItemView {
background: #333333;
color: #ffffff;
border: 1px solid #555555;
selection-background-color: #1db954;
}
""")
refresh_info_layout.addWidget(refresh_label)
refresh_info_layout.addWidget(self.refresh_interval_combo)
# Current artist display
artist_info_layout = QVBoxLayout()
@ -816,6 +875,7 @@ class MetadataUpdaterWidget(QFrame):
artist_info_layout.addWidget(self.current_artist_label)
control_layout.addWidget(self.start_button)
control_layout.addLayout(refresh_info_layout)
control_layout.addLayout(artist_info_layout)
control_layout.addStretch()
@ -857,6 +917,7 @@ class MetadataUpdaterWidget(QFrame):
progress_layout.addWidget(self.progress_bar)
layout.addWidget(header_label)
layout.addWidget(info_label)
layout.addLayout(control_layout)
layout.addLayout(progress_layout)
@ -875,6 +936,20 @@ class MetadataUpdaterWidget(QFrame):
self.progress_label.setText("Progress: 0%")
self.count_label.setText("0 / 0 artists")
self.progress_bar.setValue(0)
def get_refresh_interval_days(self) -> int:
"""Convert dropdown selection to number of days"""
interval_map = {
"6 months": 180,
"3 months": 90,
"1 month": 30,
"2 weeks": 14,
"1 week": 7,
"Full refresh": 0 # 0 means update everything
}
selected = self.refresh_interval_combo.currentText()
return interval_map.get(selected, 30) # Default to 1 month
class ActivityItem(QWidget):
def __init__(self, icon: str, title: str, subtitle: str, time: str, parent=None):
@ -1216,11 +1291,15 @@ class DashboardPage(QWidget):
return
try:
# Get refresh interval from dropdown
refresh_interval_days = self.metadata_widget.get_refresh_interval_days()
# Start the metadata update worker (it will handle artist retrieval in background)
self.metadata_worker = MetadataUpdateWorker(
None, # Artists will be loaded in the worker thread
self.data_provider.service_clients['plex'],
self.data_provider.service_clients['spotify']
self.data_provider.service_clients['spotify'],
refresh_interval_days
)
# Connect signals