Add incremental metadata/artwork sync to iPod (iTunes-style)
- content_hash() — SHA1 of first 64KB for matching local↔iPod tracks - Nano7Database.update_track_metadata() — update DB records only, never touch audio files on the iPod - Nano7Database.find_track_by_content_hash() + find_track_by_metadata() - _write_databases_from_tracks() extracted from sync_itunescdb, supports artwork_overrides parameter - LibraryCache stores content_hash for cached tracks - Worker + UI: 'Sync Metadata to iPod' button + context menu item - _REMOVE_ARTWORK sentinel for explicit artwork removal - track_info.py: add play_count, rating, last_played, skip_count fields
This commit is contained in:
+40
-3
@@ -6,6 +6,7 @@ Provides the library UI, playback controls, and transfer/conversion workflows.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
from typing import List, Dict, Optional, Tuple, Any
|
||||
from pathlib import Path
|
||||
@@ -22,7 +23,7 @@ from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
|
||||
from track_info import TrackInfo
|
||||
from metadata_handler import MetadataHandler
|
||||
from ui.metadata_editor import MetadataEditorDialog
|
||||
from library_cache import get_library_cache, invalidate_cache_for_path
|
||||
from library_cache import get_library_cache, invalidate_cache_for_path, save_library_cache
|
||||
from artwork.cache import cover_cache
|
||||
from config_loader import ConfigLoader
|
||||
from worker import WorkerThread
|
||||
@@ -74,6 +75,7 @@ class LibraryTab(QWidget):
|
||||
self._saved_position_ms: int = 0
|
||||
self._saved_playing: bool = False
|
||||
self._pending_seek_ms: int = 0
|
||||
self._halfway_passed: set[int] = set()
|
||||
|
||||
self._library_toolbar_buttons: list = []
|
||||
|
||||
@@ -163,9 +165,9 @@ class LibraryTab(QWidget):
|
||||
self._setup_library_toolbar(layout)
|
||||
|
||||
self.library_table = QTableWidget()
|
||||
self.library_table.setColumnCount(7)
|
||||
self.library_table.setColumnCount(8)
|
||||
self.library_table.setHorizontalHeaderLabels(
|
||||
["#", "Artist", "Title", "Album", "Duration", "Genre", "Size"]
|
||||
["#", "Artist", "Title", "Album", "Duration", "Genre", "Size", "Plays"]
|
||||
)
|
||||
self.library_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self.library_table.setSelectionMode(QTableWidget.SelectionMode.ExtendedSelection)
|
||||
@@ -184,6 +186,7 @@ class LibraryTab(QWidget):
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(6, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(7, QHeaderView.ResizeMode.ResizeToContents)
|
||||
|
||||
layout.addWidget(self.library_table, stretch=1)
|
||||
|
||||
@@ -281,6 +284,7 @@ class LibraryTab(QWidget):
|
||||
|
||||
old_index = self.current_playback_index
|
||||
self._clear_playing_row(old_index)
|
||||
self._halfway_passed.discard(old_index)
|
||||
|
||||
self.current_playback_index = index
|
||||
self.player.setSource(QUrl.fromLocalFile(track_data["path"]))
|
||||
@@ -407,6 +411,8 @@ class LibraryTab(QWidget):
|
||||
if duration > 0 and not self.position_slider.isSliderDown():
|
||||
self.position_slider.setValue(int(position / duration * 1000))
|
||||
self.time_label_start.setText(self._format_time_ms(position))
|
||||
if duration > 0 and position >= duration * 0.5:
|
||||
self._halfway_passed.add(self.current_playback_index)
|
||||
|
||||
def _on_media_duration_changed(self, duration: int):
|
||||
self.time_label_end.setText(self._format_time_ms(duration))
|
||||
@@ -415,9 +421,11 @@ class LibraryTab(QWidget):
|
||||
|
||||
def _on_media_status_changed(self, status):
|
||||
if status == QMediaPlayer.MediaStatus.EndOfMedia:
|
||||
self._increment_play_count()
|
||||
self._stop_eq_animation()
|
||||
self._on_next_clicked()
|
||||
elif status == QMediaPlayer.MediaStatus.NoMedia:
|
||||
self._halfway_passed.discard(self.current_playback_index)
|
||||
self._stop_eq_animation()
|
||||
self.play_btn.setText("\u25B6")
|
||||
self._is_playing = False
|
||||
@@ -426,6 +434,34 @@ class LibraryTab(QWidget):
|
||||
self.player.setPosition(self._pending_seek_ms)
|
||||
self._pending_seek_ms = 0
|
||||
|
||||
def _increment_play_count(self):
|
||||
idx = self.current_playback_index
|
||||
if idx not in self._halfway_passed:
|
||||
return
|
||||
if idx < 0 or idx >= self.library_table.rowCount():
|
||||
return
|
||||
data_item = self.library_table.item(idx, 0)
|
||||
if data_item is None:
|
||||
return
|
||||
_is_ready, track_data = data_item.data(Qt.ItemDataRole.UserRole)
|
||||
if not track_data:
|
||||
return
|
||||
path = track_data.get("path", "")
|
||||
if not path:
|
||||
return
|
||||
cache = get_library_cache()
|
||||
entry = cache.get(path)
|
||||
if entry is None:
|
||||
entry = track_data.copy()
|
||||
entry["play_count"] = entry.get("play_count", 0) + 1
|
||||
entry["last_played"] = int(time.time())
|
||||
cache.put(path, entry)
|
||||
save_library_cache()
|
||||
self._halfway_passed.discard(idx)
|
||||
plays_item = self.library_table.item(idx, 7)
|
||||
if plays_item:
|
||||
plays_item.setText(str(entry["play_count"]))
|
||||
|
||||
def _start_eq_animation(self):
|
||||
if not self._eq_timer or self._eq_running:
|
||||
return
|
||||
@@ -702,6 +738,7 @@ class LibraryTab(QWidget):
|
||||
QTableWidgetItem(duration_str),
|
||||
QTableWidgetItem(track.get("genre", "") or ""),
|
||||
QTableWidgetItem(self._format_size(track["size"])),
|
||||
QTableWidgetItem(str(track.get("play_count", "0") or "0") if is_ready else ""),
|
||||
]
|
||||
|
||||
if not is_ready:
|
||||
|
||||
Reference in New Issue
Block a user