-
-
Notifications
You must be signed in to change notification settings - Fork 419
PICARD-3118: Allow user to save/load current Picard session #2731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
zas
merged 31 commits into
metabrainz:master
from
knguyen1:feat/PICARD-3118/add-save-session-feature
Sep 30, 2025
Merged
Changes from 6 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
b049904
Complete session save/load roundtrip
knguyen1 16f21c7
Refactor sessions manager and add unit tests
knguyen1 a596368
Session files should be compressed
knguyen1 b44d36e
Enable caching of mb data in sessions
knguyen1 8383a97
Do not catch blind exceptions
knguyen1 1251e9c
Apply single quote on dict/attr keys
knguyen1 b8c0b42
Zas code review 20250908; own close session confirm box
knguyen1 7bff0c4
Use try...except...else pattern
knguyen1 86eef7a
Add flyout menu for recent sessions
knguyen1 1c14bc4
Make session_folder configurable and default
knguyen1 69ca212
Clean up menus; add default filenames
knguyen1 7b7506c
`load_session` should default to the last_session_path
knguyen1 11de913
Fix failing Windows tests
knguyen1 722b3fc
Add atomic writes; fix crash during load session
knguyen1 a644edb
Refactor ; remove redundant code
knguyen1 6da5c74
Switch from json to yaml for session files
knguyen1 f18eb91
Refactor session loader for dry/srp
knguyen1 f437291
Fix bug with blank albums (no cache, no web)
knguyen1 c3ca44e
Fix bug with web requests suppression
knguyen1 325bbc4
Fix some inconsistencies with `last_session_path`
knguyen1 847bd33
Apply zas code review recs 20250910
knguyen1 1bee982
Merge master into feat/PICARD-3118/add-save-session-feature
knguyen1 14e952b
Apply zas code review 20250924
knguyen1 31d7bc3
Rename `dont_write_tags` -> `enable_tag_saving`
knguyen1 499c052
Make `_atomic_write` its own utility
knguyen1 8eef846
Apply zas code review 20250925
knguyen1 cd61cb8
Improve type hints
knguyen1 9479f39
Make `restore_options` more generic
knguyen1 7e7654b
Fix: `dir` -> `directory`
knguyen1 3ebd96b
refactor: `session_loader` more readable
knguyen1 1c189b0
Refactor: Centralize RESTORABLE_CONFIG_KEYS for session management
knguyen1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Picard, the next-generation MusicBrainz tagger | ||
# | ||
# Copyright (C) 2025 The MusicBrainz Team | ||
# | ||
# This program is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License | ||
# as published by the Free Software Foundation; either version 2 | ||
# of the License, or (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program; if not, write to the Free Software | ||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
|
||
"""Session management package for Picard.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Picard, the next-generation MusicBrainz tagger | ||
# | ||
# Copyright (C) 2025 The MusicBrainz Team | ||
# | ||
# This program is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License | ||
# as published by the Free Software Foundation; either version 2 | ||
# of the License, or (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program; if not, write to the Free Software | ||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
|
||
"""Constants for session management. | ||
This module contains all constants used throughout the session management system, | ||
including retry delays, file extensions, and excluded tags. | ||
""" | ||
|
||
|
||
class SessionConstants: | ||
"""Constants for session management operations.""" | ||
|
||
# File handling | ||
SESSION_FILE_EXTENSION = ".mbps.gz" | ||
SESSION_FORMAT_VERSION = 1 | ||
|
||
# Retry delays in milliseconds | ||
zas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
DEFAULT_RETRY_DELAY_MS = 200 | ||
FAST_RETRY_DELAY_MS = 150 | ||
|
||
# Metadata handling | ||
INTERNAL_TAG_PREFIX = "~" | ||
EXCLUDED_OVERRIDE_TAGS = frozenset({"length", "~length"}) | ||
zas marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
# Location types | ||
LOCATION_UNCLUSTERED = "unclustered" | ||
LOCATION_TRACK = "track" | ||
LOCATION_ALBUM_UNMATCHED = "album_unmatched" | ||
LOCATION_CLUSTER = "cluster" | ||
LOCATION_NAT = "nat" | ||
|
||
|
||
class SessionMessages: | ||
"""Centralized session-related message strings. | ||
Define raw, untranslated strings. Call sites should mark for translation: | ||
- API/config titles: wrap with N_() | ||
- UI labels: wrap with _() | ||
""" | ||
|
||
# Option titles (API/config) | ||
SESSION_SAFE_RESTORE_TITLE = "Honor local edits and placement on load (no auto-matching)" | ||
SESSION_LOAD_LAST_TITLE = "Load last saved session on startup" | ||
SESSION_AUTOSAVE_TITLE = "Auto-save session every N minutes (0 disables)" | ||
SESSION_BACKUP_TITLE = "Attempt to keep a session backup on unexpected shutdown" | ||
SESSION_INCLUDE_MB_DATA_TITLE = "Include MusicBrainz data in saved sessions (faster loads, risk of stale data)" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Picard, the next-generation MusicBrainz tagger | ||
# | ||
# Copyright (C) 2025 The MusicBrainz Team | ||
# | ||
# This program is free software; you can redistribute it and/or | ||
# modify it under the terms of the GNU General Public License | ||
# as published by the Free Software Foundation; either version 2 | ||
# of the License, or (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program; if not, write to the Free Software | ||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
|
||
"""Location detection for session management. | ||
|
||
This module handles detecting where files should be placed within a session, | ||
separating the complex location detection logic from other concerns. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
from picard.album import Album, NatAlbum | ||
from picard.cluster import Cluster, UnclusteredFiles | ||
from picard.file import File | ||
from picard.session.constants import SessionConstants | ||
from picard.session.session_data import SessionItemLocation | ||
from picard.track import Track | ||
|
||
|
||
class LocationDetector: | ||
"""Detects the location type of files in the session.""" | ||
|
||
def detect(self, file: File) -> SessionItemLocation: | ||
"""Detect where a file should be placed in the session. | ||
|
||
Parameters | ||
---------- | ||
file : File | ||
The file to detect the location for. | ||
|
||
Returns | ||
------- | ||
SessionItemLocation | ||
The location information for the file. | ||
|
||
Notes | ||
----- | ||
This method analyzes the file's parent item to determine its proper | ||
location within the session structure. | ||
""" | ||
parent = file.parent_item | ||
if parent is None: | ||
return self._unclustered_location() | ||
|
||
if self._is_track_parent(parent): | ||
return self._detect_track_location(parent) | ||
elif self._is_cluster_parent(parent): | ||
return self._detect_cluster_location(parent) | ||
else: | ||
return self._unclustered_location() | ||
|
||
def _is_track_parent(self, parent: object) -> bool: | ||
"""Check if parent is a track (has album attribute). | ||
|
||
Parameters | ||
---------- | ||
parent : object | ||
The parent item to check. | ||
|
||
Returns | ||
------- | ||
bool | ||
True if parent is a track. | ||
""" | ||
return hasattr(parent, 'album') and isinstance(parent.album, Album) | ||
|
||
def _is_cluster_parent(self, parent: object) -> bool: | ||
"""Check if parent is a cluster. | ||
|
||
Parameters | ||
---------- | ||
parent : object | ||
The parent item to check. | ||
|
||
Returns | ||
------- | ||
bool | ||
True if parent is a cluster. | ||
""" | ||
return isinstance(parent, Cluster) | ||
|
||
def _detect_track_location(self, parent: Track) -> SessionItemLocation: | ||
"""Detect location for files under a track. | ||
|
||
Parameters | ||
---------- | ||
parent : Track | ||
The track parent item. | ||
|
||
Returns | ||
------- | ||
SessionItemLocation | ||
The location information for the track. | ||
""" | ||
if isinstance(parent.album, NatAlbum): | ||
# NAT special handling | ||
return SessionItemLocation(type=SessionConstants.LOCATION_NAT, recording_id=parent.id) | ||
|
||
# Track placement | ||
if hasattr(parent, 'id') and parent.id: | ||
return SessionItemLocation( | ||
type=SessionConstants.LOCATION_TRACK, album_id=parent.album.id, recording_id=parent.id | ||
) | ||
|
||
# Fallback to album unmatched | ||
return SessionItemLocation(type=SessionConstants.LOCATION_ALBUM_UNMATCHED, album_id=parent.album.id) | ||
|
||
def _detect_cluster_location(self, parent: Cluster) -> SessionItemLocation: | ||
"""Detect location for files under a cluster. | ||
|
||
Parameters | ||
---------- | ||
parent : Cluster | ||
The cluster parent item. | ||
|
||
Returns | ||
------- | ||
SessionItemLocation | ||
The location information for the cluster. | ||
""" | ||
# Unmatched files inside an album | ||
if parent.related_album: | ||
return SessionItemLocation(type=SessionConstants.LOCATION_ALBUM_UNMATCHED, album_id=parent.related_album.id) | ||
|
||
# Left pane cluster | ||
if isinstance(parent, UnclusteredFiles): | ||
return self._unclustered_location() | ||
|
||
return SessionItemLocation( | ||
type=SessionConstants.LOCATION_CLUSTER, | ||
cluster_title=str(parent.metadata['album']), | ||
cluster_artist=str(parent.metadata['albumartist']), | ||
) | ||
|
||
def _unclustered_location(self) -> SessionItemLocation: | ||
"""Create an unclustered location. | ||
|
||
Returns | ||
------- | ||
SessionItemLocation | ||
Location for unclustered files. | ||
""" | ||
return SessionItemLocation(type=SessionConstants.LOCATION_UNCLUSTERED) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.