good
This commit is contained in:
parent
6e5a0f76f6
commit
fc87fe1740
7 changed files with 4137 additions and 16 deletions
|
|
@ -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"}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
3919
logs/app.log
3919
logs/app.log
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue