rekordbox-edit 0.4.0.dev39__tar.gz → 0.4.0.dev40__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/CHANGELOG.md +3 -1
  2. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/PKG-INFO +4 -4
  3. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/README.md +3 -3
  4. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/pyproject.toml +1 -1
  5. rekordbox_edit-0.4.0.dev40/rekordbox_edit/args.py +54 -0
  6. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/commands/convert.py +16 -15
  7. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/commands/edit.py +16 -15
  8. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/commands/search.py +20 -49
  9. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/query.py +31 -54
  10. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/tests/commands/test_convert.py +10 -10
  11. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/tests/commands/test_edit.py +4 -4
  12. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/tests/commands/test_search.py +12 -12
  13. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/tests/test_query.py +26 -23
  14. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/uv.lock +1 -1
  15. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.agent-style/RULES.md +0 -0
  16. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.agent-style/claude-code.md +0 -0
  17. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.github/actions/commitizen-bump/action.yml +0 -0
  18. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.github/actions/commitizen-bump/commitizen-bump.sh +0 -0
  19. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.github/actions/install/action.yml +0 -0
  20. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.github/actions/lint/action.yml +0 -0
  21. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.github/actions/test/action.yml +0 -0
  22. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.github/workflows/cd.yml +0 -0
  23. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.github/workflows/ci.yml +0 -0
  24. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.github/workflows/publish.yml +0 -0
  25. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.github/workflows/release.yml +0 -0
  26. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.gitignore +0 -0
  27. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/.pre-commit-config.yaml +0 -0
  28. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/AGENTS.md +0 -0
  29. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/CLAUDE.md +0 -0
  30. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/CONTRIBUTING.md +0 -0
  31. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/LICENSE +0 -0
  32. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/Makefile +0 -0
  33. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/codecov.yml +0 -0
  34. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/__init__.py +0 -0
  35. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/_click.py +0 -0
  36. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/cli.py +0 -0
  37. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/commands/__init__.py +0 -0
  38. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/display.py +0 -0
  39. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/logger.py +0 -0
  40. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/rekordbox_edit/utils.py +0 -0
  41. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/renovate.json5 +0 -0
  42. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/ruff.toml +0 -0
  43. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/tests/__init__.py +0 -0
  44. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/tests/commands/__init__.py +0 -0
  45. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/tests/conftest.py +0 -0
  46. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/tests/test_display.py +0 -0
  47. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/tests/test_logger.py +0 -0
  48. {rekordbox_edit-0.4.0.dev39 → rekordbox_edit-0.4.0.dev40}/tests/test_utils.py +0 -0
@@ -1,6 +1,8 @@
1
- ## v0.4.0.dev39 (2026-06-05)
1
+ ## v0.4.0.dev40 (2026-06-05)
2
2
 
3
3
 
4
+ - docs: update README.md
5
+ - refactor(query): group filter args into FilterArgs dataclass
4
6
  - refactor(cli): extract convert-specific options into convert_click_options
5
7
  - refactor(cli): extract edit-specific options into edit_click_options
6
8
  - refactor(cli): extract shared confirmation flags into global_click_confirmations
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rekordbox-edit
3
- Version: 0.4.0.dev39
3
+ Version: 0.4.0.dev40
4
4
  Summary: Tools for managing and modifying a RekordBox library en-masse
5
5
  Project-URL: Homepage, https://github.com/jviall/rekordbox-edit
6
6
  Project-URL: Repository, https://github.com/jviall/rekordbox-edit
@@ -244,9 +244,9 @@ And generally limit the potential impact of a mistake by using filters to target
244
244
 
245
245
  ## AI Usage
246
246
 
247
- I believe it's important to be aware of and to disclose AI usage. AI seems to be forced upon us without us having much choice in the matter, and I like many others find this to be a gross and oppressive experience. While it has lots of potential to benefit the common good, so far it has mostly furthered capitalist greed.
247
+ I believe it's important to be aware of and to disclose AI usage. In many ways it's being forced upon us without us having much choice in the matter, and it's a gross and oppressive experience. While it has lots of potential to benefit the common good, mostly it's only furthered capitalist greed.
248
248
 
249
- This isn't a soapbox, but an attempt to thoughtfully disclose how generative AI has contributed to building this tool. I might have built it without AI, but it's not usually my preference to spend my free time coding in front of a computer, and using it has drastically reduced the time it would have taken. I have many years of professional experience as a Software Engineer, and while it will probably age poorly to say this in the (hopefully far) future, I don't think AI could have built this tool without me. Please validate the quality of this project yourself--at the end of the day it's just code written by some stranger!
249
+ I'm mostly attempting to thoughtfully disclose that generative AI _has_ been a significant tool in building out this project. I don't personally enjoy too much coding in my personal time, but I feel passionate about making `rekordbox-edit`--AI has admittedly helped me bridge that gap between my capacity and my vision. I'm a career professional software engineer who takes pride in their work, and I don't want to produce a vibe coded mess any more than you want to experience it. Please validate the quality of this project yourself--at the end of the day it's just code written by some stranger.
250
250
 
251
251
  If it's any consolation, my main test subject has been my own 10,000+ track RekordBox library--a risk I do not take lightly!
252
252
 
@@ -254,7 +254,7 @@ If it's any consolation, my main test subject has been my own 10,000+ track Reko
254
254
 
255
255
  This project exists thanks to [@dylanjones](https://github.com/dylanjones), the creator of [pyrekordbox](https://github.com/dylanljones/pyrekordbox), which provides the Python API for Rekordbox databases.
256
256
 
257
- I built this tool to help correct my own bad habits and misteps in managing and organizing my rekordbox library. If it helps you too, great! If you find issues or have ideas, contributions are welcome.
257
+ I built this tool to help correct my own bad habits and missteps in managing and organizing my rekordbox library. If it helps you too, great! If you find issues or have ideas, contributions are welcome.
258
258
 
259
259
  ## Contributing
260
260
 
@@ -225,9 +225,9 @@ And generally limit the potential impact of a mistake by using filters to target
225
225
 
226
226
  ## AI Usage
227
227
 
228
- I believe it's important to be aware of and to disclose AI usage. AI seems to be forced upon us without us having much choice in the matter, and I like many others find this to be a gross and oppressive experience. While it has lots of potential to benefit the common good, so far it has mostly furthered capitalist greed.
228
+ I believe it's important to be aware of and to disclose AI usage. In many ways it's being forced upon us without us having much choice in the matter, and it's a gross and oppressive experience. While it has lots of potential to benefit the common good, mostly it's only furthered capitalist greed.
229
229
 
230
- This isn't a soapbox, but an attempt to thoughtfully disclose how generative AI has contributed to building this tool. I might have built it without AI, but it's not usually my preference to spend my free time coding in front of a computer, and using it has drastically reduced the time it would have taken. I have many years of professional experience as a Software Engineer, and while it will probably age poorly to say this in the (hopefully far) future, I don't think AI could have built this tool without me. Please validate the quality of this project yourself--at the end of the day it's just code written by some stranger!
230
+ I'm mostly attempting to thoughtfully disclose that generative AI _has_ been a significant tool in building out this project. I don't personally enjoy too much coding in my personal time, but I feel passionate about making `rekordbox-edit`--AI has admittedly helped me bridge that gap between my capacity and my vision. I'm a career professional software engineer who takes pride in their work, and I don't want to produce a vibe coded mess any more than you want to experience it. Please validate the quality of this project yourself--at the end of the day it's just code written by some stranger.
231
231
 
232
232
  If it's any consolation, my main test subject has been my own 10,000+ track RekordBox library--a risk I do not take lightly!
233
233
 
@@ -235,7 +235,7 @@ If it's any consolation, my main test subject has been my own 10,000+ track Reko
235
235
 
236
236
  This project exists thanks to [@dylanjones](https://github.com/dylanjones), the creator of [pyrekordbox](https://github.com/dylanljones/pyrekordbox), which provides the Python API for Rekordbox databases.
237
237
 
238
- I built this tool to help correct my own bad habits and misteps in managing and organizing my rekordbox library. If it helps you too, great! If you find issues or have ideas, contributions are welcome.
238
+ I built this tool to help correct my own bad habits and missteps in managing and organizing my rekordbox library. If it helps you too, great! If you find issues or have ideas, contributions are welcome.
239
239
 
240
240
  ## Contributing
241
241
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rekordbox-edit"
7
- version = "0.4.0.dev39"
7
+ version = "0.4.0.dev40"
8
8
  description = "Tools for managing and modifying a RekordBox library en-masse"
9
9
  authors = [{ name = "James Viall", email= "jamesviall@pm.me"}]
10
10
  license = "MIT"
@@ -0,0 +1,54 @@
1
+ """Dataclass containers for CLI argument groups.
2
+
3
+ These types are the public API of the functional layer below the CLI: callers
4
+ of `get_filtered_content` and the private command helpers receive them in lieu
5
+ of long flat parameter lists. Each `*_from_kwargs` factory packs the matching
6
+ Click parameters into its dataclass.
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+
11
+
12
+ @dataclass
13
+ class FilterArgs:
14
+ """Filter inputs forwarded to `get_filtered_content`.
15
+
16
+ Field names mirror the Click parameter names: `track_ids` holds the
17
+ positional TRACK_IDS argument (variadic), `track_id` holds the values of
18
+ the repeated `--track-id` option.
19
+ """
20
+
21
+ track_id: list[str] = field(default_factory=list)
22
+ track_ids: list[str] = field(default_factory=list)
23
+ title: list[str] = field(default_factory=list)
24
+ exact_title: list[str] = field(default_factory=list)
25
+ playlist: list[str] = field(default_factory=list)
26
+ exact_playlist: list[str] = field(default_factory=list)
27
+ artist: list[str] = field(default_factory=list)
28
+ exact_artist: list[str] = field(default_factory=list)
29
+ album: list[str] = field(default_factory=list)
30
+ exact_album: list[str] = field(default_factory=list)
31
+ path: list[str] = field(default_factory=list)
32
+ exact_path: list[str] = field(default_factory=list)
33
+ format: list[str] = field(default_factory=list)
34
+ match_all: bool = False
35
+
36
+
37
+ def filter_args_from_kwargs(**kwargs) -> FilterArgs:
38
+ """Pack the flat Click kwargs for the `global_click_filters` group into a FilterArgs."""
39
+ return FilterArgs(
40
+ track_id=list(kwargs.get("track_id") or []),
41
+ track_ids=list(kwargs.get("track_ids") or []),
42
+ title=list(kwargs.get("title") or []),
43
+ exact_title=list(kwargs.get("exact_title") or []),
44
+ playlist=list(kwargs.get("playlist") or []),
45
+ exact_playlist=list(kwargs.get("exact_playlist") or []),
46
+ artist=list(kwargs.get("artist") or []),
47
+ exact_artist=list(kwargs.get("exact_artist") or []),
48
+ album=list(kwargs.get("album") or []),
49
+ exact_album=list(kwargs.get("exact_album") or []),
50
+ path=list(kwargs.get("path") or []),
51
+ exact_path=list(kwargs.get("exact_path") or []),
52
+ format=list(kwargs.get("format") or []),
53
+ match_all=bool(kwargs.get("match_all", False)),
54
+ )
@@ -22,6 +22,7 @@ from rekordbox_edit._click import (
22
22
  print_option,
23
23
  track_ids_argument,
24
24
  )
25
+ from rekordbox_edit.args import filter_args_from_kwargs
25
26
  from rekordbox_edit.logger import get_debug_file_path, set_level
26
27
  from rekordbox_edit.query import get_filtered_content
27
28
  from rekordbox_edit.display import PrintableField, print_track_info
@@ -361,23 +362,23 @@ def convert_command(
361
362
  logger.debug("Database connection established")
362
363
 
363
364
  # === QUERY & FILTER ===
364
- result = get_filtered_content(
365
- db,
366
- track_id_args=track_ids,
367
- track_ids=track_id,
368
- formats=format,
369
- playlists=playlist,
370
- exact_playlists=exact_playlist,
371
- artists=artist,
372
- exact_artists=exact_artist,
373
- albums=album,
374
- exact_albums=exact_album,
375
- titles=title,
376
- exact_titles=exact_title,
377
- paths=path,
378
- exact_paths=exact_path,
365
+ filters = filter_args_from_kwargs(
366
+ track_id=track_id,
367
+ track_ids=track_ids,
368
+ playlist=playlist,
369
+ exact_playlist=exact_playlist,
370
+ album=album,
371
+ exact_album=exact_album,
372
+ artist=artist,
373
+ exact_artist=exact_artist,
374
+ title=title,
375
+ exact_title=exact_title,
376
+ path=path,
377
+ exact_path=exact_path,
378
+ format=format,
379
379
  match_all=match_all,
380
380
  )
381
+ result = get_filtered_content(db, filters)
381
382
  filtered_content = result.scalars().all()
382
383
  logger.debug(f"Query returned {len(filtered_content)} tracks")
383
384
 
@@ -16,6 +16,7 @@ from rekordbox_edit._click import (
16
16
  print_option,
17
17
  track_ids_argument,
18
18
  )
19
+ from rekordbox_edit.args import filter_args_from_kwargs
19
20
  from rekordbox_edit.logger import get_debug_file_path, set_level
20
21
  from rekordbox_edit.query import get_filtered_content
21
22
  from rekordbox_edit.display import PrintableField, print_track_info
@@ -106,23 +107,23 @@ def edit_command(
106
107
  if not db.session:
107
108
  raise RuntimeError("Failed to connect to Rekordbox Database: No Session.")
108
109
 
109
- result = get_filtered_content(
110
- db,
111
- track_id_args=track_ids,
112
- track_ids=track_id,
113
- playlists=playlist,
114
- exact_playlists=exact_playlist,
115
- artists=artist,
116
- exact_artists=exact_artist,
117
- albums=album,
118
- exact_albums=exact_album,
119
- titles=title,
120
- exact_titles=exact_title,
121
- paths=path,
122
- exact_paths=exact_path,
123
- formats=format,
110
+ filters = filter_args_from_kwargs(
111
+ track_id=track_id,
112
+ track_ids=track_ids,
113
+ playlist=playlist,
114
+ exact_playlist=exact_playlist,
115
+ album=album,
116
+ exact_album=exact_album,
117
+ artist=artist,
118
+ exact_artist=exact_artist,
119
+ title=title,
120
+ exact_title=exact_title,
121
+ path=path,
122
+ exact_path=exact_path,
123
+ format=format,
124
124
  match_all=match_all,
125
125
  )
126
+ result = get_filtered_content(db, filters)
126
127
  tracks = result.scalars().all()
127
128
 
128
129
  col_name = FIELD_COLUMNS[field]
@@ -14,6 +14,7 @@ from rekordbox_edit._click import (
14
14
  print_option,
15
15
  track_ids_argument,
16
16
  )
17
+ from rekordbox_edit.args import filter_args_from_kwargs
17
18
  from rekordbox_edit.logger import get_debug_file_path, set_level
18
19
  from rekordbox_edit.query import get_filtered_content
19
20
  from rekordbox_edit.display import print_track_info
@@ -51,61 +52,31 @@ def search_command(
51
52
  if stdin_data:
52
53
  track_ids = list(track_ids or []) + stdin_data.split()
53
54
 
54
- logger.debug("Connecting to RekordBox database...")
55
+ filters = filter_args_from_kwargs(
56
+ track_id=track_id,
57
+ track_ids=track_ids,
58
+ playlist=playlist,
59
+ exact_playlist=exact_playlist,
60
+ album=album,
61
+ exact_album=exact_album,
62
+ artist=artist,
63
+ exact_artist=exact_artist,
64
+ title=title,
65
+ exact_title=exact_title,
66
+ path=path,
67
+ exact_path=exact_path,
68
+ format=format,
69
+ match_all=match_all,
70
+ )
55
71
 
56
- # Log search parameters for troubleshooting
57
- filters = []
58
- if track_ids:
59
- filters.append(f"track_ids={track_ids}")
60
- if track_id:
61
- filters.append(f"track_id={track_id}")
62
- if artist:
63
- filters.append(f"artist={artist}")
64
- if exact_artist:
65
- filters.append(f"exact_artist={exact_artist}")
66
- if title:
67
- filters.append(f"title={title}")
68
- if exact_title:
69
- filters.append(f"exact_title={exact_title}")
70
- if album:
71
- filters.append(f"album={album}")
72
- if exact_album:
73
- filters.append(f"exact_album={exact_album}")
74
- if playlist:
75
- filters.append(f"playlist={playlist}")
76
- if exact_playlist:
77
- filters.append(f"exact_playlist={exact_playlist}")
78
- if path:
79
- filters.append(f"path={path}")
80
- if exact_path:
81
- filters.append(f"exact_path={exact_path}")
82
- if format:
83
- filters.append(f"format={format}")
84
- if match_all:
85
- filters.append("match_all=True")
86
- logger.debug(f"Search filters: {', '.join(filters) if filters else 'none'}")
72
+ logger.debug(f"Search filters: {filters}")
73
+ logger.debug("Connecting to RekordBox database...")
87
74
 
88
75
  db = Rekordbox6Database()
89
76
  if not db.session:
90
77
  raise RuntimeError("Failed to connect to Rekordbox Database: No Session.")
91
78
 
92
- filtered_result = get_filtered_content(
93
- db,
94
- track_id_args=track_ids,
95
- track_ids=track_id,
96
- playlists=playlist,
97
- exact_playlists=exact_playlist,
98
- artists=artist,
99
- exact_artists=exact_artist,
100
- albums=album,
101
- exact_albums=exact_album,
102
- titles=title,
103
- exact_titles=exact_title,
104
- paths=path,
105
- exact_paths=exact_path,
106
- formats=format,
107
- match_all=match_all,
108
- )
79
+ filtered_result = get_filtered_content(db, filters)
109
80
 
110
81
  if print_opt is PrintChoice.SILENT:
111
82
  pass
@@ -13,6 +13,8 @@ from pyrekordbox.db6.tables import (
13
13
  from sqlalchemy import ColumnElement, Result, and_, func, or_, select
14
14
  from sqlalchemy.orm import aliased
15
15
 
16
+ from rekordbox_edit.args import FilterArgs
17
+
16
18
  logger = logging.getLogger(__name__)
17
19
 
18
20
 
@@ -247,20 +249,7 @@ class CollectionQuery:
247
249
 
248
250
  def get_filtered_content(
249
251
  db: Rekordbox6Database,
250
- track_id_args: List[str] | None = None,
251
- track_ids: List[str] | None = None,
252
- formats: List[str] | None = None,
253
- playlists: List[str] | None = None,
254
- exact_playlists: List[str] | None = None,
255
- artists: List[str] | None = None,
256
- exact_artists: List[str] | None = None,
257
- albums: List[str] | None = None,
258
- exact_albums: List[str] | None = None,
259
- titles: List[str] | None = None,
260
- exact_titles: List[str] | None = None,
261
- paths: List[str] | None = None,
262
- exact_paths: List[str] | None = None,
263
- match_all: bool = False,
252
+ filters: FilterArgs,
264
253
  ) -> Result[Tuple[DjmdContent]]:
265
254
  """Query the Rekordbox database with the provided filters."""
266
255
  db = db if db is not None else Rekordbox6Database()
@@ -269,59 +258,47 @@ def get_filtered_content(
269
258
 
270
259
  query = CollectionQuery()
271
260
 
272
- if track_id_args:
273
- logger.debug(f"Filtering by {len(track_id_args)} track ID argument(s)")
274
- query = query.by_track_ids(track_ids=track_id_args)
261
+ if filters.track_ids:
262
+ logger.debug(f"Filtering by {len(filters.track_ids)} track ID argument(s)")
263
+ query = query.by_track_ids(track_ids=filters.track_ids)
275
264
 
276
- if track_ids:
277
- for track_id in track_ids:
278
- query = query.by_track_ids(track_id)
265
+ for tid in filters.track_id:
266
+ query = query.by_track_ids(tid)
279
267
 
280
- if formats:
281
- for fmt in formats:
282
- query = query.by_format(fmt)
268
+ for fmt in filters.format:
269
+ query = query.by_format(fmt)
283
270
 
284
- if playlists:
285
- for playlist in playlists:
286
- query = query.by_playlist(playlist)
271
+ for playlist in filters.playlist:
272
+ query = query.by_playlist(playlist)
287
273
 
288
- if exact_playlists:
289
- for exact_playlist in exact_playlists:
290
- query = query.by_playlist(exact_playlist, exact=True)
274
+ for exact_playlist in filters.exact_playlist:
275
+ query = query.by_playlist(exact_playlist, exact=True)
291
276
 
292
- if artists:
293
- for artist in artists:
294
- query = query.by_artist(artist)
277
+ for artist in filters.artist:
278
+ query = query.by_artist(artist)
295
279
 
296
- if exact_artists:
297
- for exact_artist in exact_artists:
298
- query = query.by_artist(exact_artist, exact=True)
280
+ for exact_artist in filters.exact_artist:
281
+ query = query.by_artist(exact_artist, exact=True)
299
282
 
300
- if albums:
301
- for album in albums:
302
- query = query.by_album(album)
283
+ for album in filters.album:
284
+ query = query.by_album(album)
303
285
 
304
- if exact_albums:
305
- for exact_album in exact_albums:
306
- query = query.by_album(exact_album, exact=True)
286
+ for exact_album in filters.exact_album:
287
+ query = query.by_album(exact_album, exact=True)
307
288
 
308
- if titles:
309
- for title in titles:
310
- query = query.by_title(title)
289
+ for title in filters.title:
290
+ query = query.by_title(title)
311
291
 
312
- if exact_titles:
313
- for title in exact_titles:
314
- query = query.by_title(title, exact=True)
292
+ for title in filters.exact_title:
293
+ query = query.by_title(title, exact=True)
315
294
 
316
- if paths:
317
- for path in paths:
318
- query = query.by_path(path)
295
+ for path in filters.path:
296
+ query = query.by_path(path)
319
297
 
320
- if exact_paths:
321
- for exact_path in exact_paths:
322
- query = query.by_path(exact_path, exact=True)
298
+ for exact_path in filters.exact_path:
299
+ query = query.by_path(exact_path, exact=True)
323
300
 
324
- if match_all:
301
+ if filters.match_all:
325
302
  query = query.match_all()
326
303
 
327
304
  return query.execute(db)
@@ -709,10 +709,10 @@ class TestConvertCommand:
709
709
  ["--dry-run", "--artist", "Daft Punk", "--format", "flac", "--match-all"],
710
710
  )
711
711
 
712
- call_kwargs = mock_get_filtered_content.call_args.kwargs
713
- assert call_kwargs["artists"] == ("Daft Punk",)
714
- assert call_kwargs["formats"] == ("flac",)
715
- assert call_kwargs["match_all"] is True
712
+ filters = mock_get_filtered_content.call_args.args[1]
713
+ assert filters.artist == ["Daft Punk"]
714
+ assert filters.format == ["flac"]
715
+ assert filters.match_all is True
716
716
 
717
717
  @patch("rekordbox_edit.commands.convert.get_rekordbox_pid")
718
718
  @patch("rekordbox_edit.commands.convert.confirm")
@@ -2074,8 +2074,8 @@ class TestConvertStdinPiping:
2074
2074
  input="190993005 108916663 59476253",
2075
2075
  )
2076
2076
 
2077
- call_kwargs = mock_get_filtered_content.call_args.kwargs
2078
- assert call_kwargs["track_id_args"] == ["190993005", "108916663", "59476253"]
2077
+ filters = mock_get_filtered_content.call_args.args[1]
2078
+ assert filters.track_ids == ["190993005", "108916663", "59476253"]
2079
2079
 
2080
2080
  @patch("rekordbox_edit.commands.convert.get_rekordbox_pid")
2081
2081
  @patch("rekordbox_edit.commands.convert.get_filtered_content")
@@ -2109,8 +2109,8 @@ class TestConvertStdinPiping:
2109
2109
  input="59476253 113475696",
2110
2110
  )
2111
2111
 
2112
- call_kwargs = mock_get_filtered_content.call_args.kwargs
2113
- assert call_kwargs["track_id_args"] == [
2112
+ filters = mock_get_filtered_content.call_args.args[1]
2113
+ assert filters.track_ids == [
2114
2114
  "190993005",
2115
2115
  "108916663",
2116
2116
  "59476253",
@@ -2154,5 +2154,5 @@ class TestConvertStdinPiping:
2154
2154
  runner = CliRunner()
2155
2155
  runner.invoke(convert_command, ["--dry-run"], input=" ")
2156
2156
 
2157
- call_kwargs = mock_get_filtered_content.call_args.kwargs
2158
- assert not call_kwargs["track_id_args"]
2157
+ filters = mock_get_filtered_content.call_args.args[1]
2158
+ assert not filters.track_ids
@@ -219,10 +219,10 @@ class TestEditCommandPhase1:
219
219
  ],
220
220
  )
221
221
 
222
- kwargs = mock_gfc.call_args.kwargs
223
- assert kwargs["artists"] == ("Bicep",)
224
- assert kwargs["formats"] == ("flac",)
225
- assert kwargs["match_all"] is True
222
+ filters = mock_gfc.call_args.args[1]
223
+ assert filters.artist == ["Bicep"]
224
+ assert filters.format == ["flac"]
225
+ assert filters.match_all is True
226
226
 
227
227
  @patch("rekordbox_edit.commands.edit.confirm")
228
228
  @patch("rekordbox_edit.commands.edit.get_filtered_content")
@@ -150,12 +150,12 @@ class TestSearchCommand:
150
150
  ],
151
151
  )
152
152
 
153
- call_kwargs = mock_get_filtered_content.call_args.kwargs
154
- assert call_kwargs["artists"] == ("Daft Punk",)
155
- assert call_kwargs["formats"] == ("flac",)
156
- assert call_kwargs["paths"] == ("song.mp3",)
157
- assert call_kwargs["exact_paths"] == ("/Music/album/track.wav",)
158
- assert call_kwargs["match_all"] is True
153
+ filters = mock_get_filtered_content.call_args.args[1]
154
+ assert filters.artist == ["Daft Punk"]
155
+ assert filters.format == ["flac"]
156
+ assert filters.path == ["song.mp3"]
157
+ assert filters.exact_path == ["/Music/album/track.wav"]
158
+ assert filters.match_all is True
159
159
 
160
160
 
161
161
  class TestSearchStdinPiping:
@@ -184,8 +184,8 @@ class TestSearchStdinPiping:
184
184
  runner = CliRunner()
185
185
  runner.invoke(search_command, [], input="190993005 108916663 59476253")
186
186
 
187
- call_kwargs = mock_get_filtered_content.call_args.kwargs
188
- assert call_kwargs["track_id_args"] == ["190993005", "108916663", "59476253"]
187
+ filters = mock_get_filtered_content.call_args.args[1]
188
+ assert filters.track_ids == ["190993005", "108916663", "59476253"]
189
189
 
190
190
  @patch("rekordbox_edit.commands.search.print_track_info")
191
191
  @patch("rekordbox_edit.commands.search.get_filtered_content")
@@ -214,8 +214,8 @@ class TestSearchStdinPiping:
214
214
  input="59476253 113475696",
215
215
  )
216
216
 
217
- call_kwargs = mock_get_filtered_content.call_args.kwargs
218
- assert call_kwargs["track_id_args"] == [
217
+ filters = mock_get_filtered_content.call_args.args[1]
218
+ assert filters.track_ids == [
219
219
  "190993005",
220
220
  "108916663",
221
221
  "59476253",
@@ -245,5 +245,5 @@ class TestSearchStdinPiping:
245
245
  runner = CliRunner()
246
246
  runner.invoke(search_command, [], input=" ")
247
247
 
248
- call_kwargs = mock_get_filtered_content.call_args.kwargs
249
- assert not call_kwargs["track_id_args"]
248
+ filters = mock_get_filtered_content.call_args.args[1]
249
+ assert not filters.track_ids
@@ -9,6 +9,7 @@ from unittest.mock import MagicMock
9
9
  import pytest
10
10
  from sqlalchemy import ColumnElement
11
11
 
12
+ from rekordbox_edit.args import FilterArgs
12
13
  from rekordbox_edit.query import CollectionQuery, get_filtered_content
13
14
 
14
15
 
@@ -590,7 +591,7 @@ class TestGetFilteredContent:
590
591
  """Tests for the get_filtered_content function."""
591
592
 
592
593
  def test_no_filters(self, mock_db, mock_query):
593
- get_filtered_content(mock_db)
594
+ get_filtered_content(mock_db, FilterArgs())
594
595
  mock_query.by_track_ids.assert_not_called()
595
596
  mock_query.by_artist.assert_not_called()
596
597
  mock_query.by_title.assert_not_called()
@@ -601,83 +602,84 @@ class TestGetFilteredContent:
601
602
  mock_query.execute.assert_called_once_with(mock_db)
602
603
 
603
604
  def test_track_id_args(self, mock_db, mock_query):
604
- get_filtered_content(mock_db, track_id_args=["123", "456"])
605
+ get_filtered_content(mock_db, FilterArgs(track_ids=["123", "456"]))
605
606
  mock_query.by_track_ids.assert_called_once_with(track_ids=["123", "456"])
606
607
 
607
608
  def test_track_ids(self, mock_db, mock_query):
608
- get_filtered_content(mock_db, track_ids=["123", "456"])
609
+ get_filtered_content(mock_db, FilterArgs(track_id=["123", "456"]))
609
610
  assert mock_query.by_track_ids.call_count == 2
610
611
  mock_query.by_track_ids.assert_any_call("123")
611
612
  mock_query.by_track_ids.assert_any_call("456")
612
613
 
613
614
  def test_artist(self, mock_db, mock_query):
614
- get_filtered_content(mock_db, artists=["Daft Punk"])
615
+ get_filtered_content(mock_db, FilterArgs(artist=["Daft Punk"]))
615
616
  mock_query.by_artist.assert_called_once_with("Daft Punk")
616
617
 
617
618
  def test_multiple_artists(self, mock_db, mock_query):
618
- get_filtered_content(mock_db, artists=["Daft Punk", "Justice"])
619
+ get_filtered_content(mock_db, FilterArgs(artist=["Daft Punk", "Justice"]))
619
620
  assert mock_query.by_artist.call_count == 2
620
621
  mock_query.by_artist.assert_any_call("Daft Punk")
621
622
  mock_query.by_artist.assert_any_call("Justice")
622
623
 
623
624
  def test_exact_artist(self, mock_db, mock_query):
624
- get_filtered_content(mock_db, exact_artists=["Daft Punk"])
625
+ get_filtered_content(mock_db, FilterArgs(exact_artist=["Daft Punk"]))
625
626
  mock_query.by_artist.assert_called_once_with("Daft Punk", exact=True)
626
627
 
627
628
  def test_title(self, mock_db, mock_query):
628
- get_filtered_content(mock_db, titles=["One More Time"])
629
+ get_filtered_content(mock_db, FilterArgs(title=["One More Time"]))
629
630
  mock_query.by_title.assert_called_once_with("One More Time")
630
631
 
631
632
  def test_exact_title(self, mock_db, mock_query):
632
- get_filtered_content(mock_db, exact_titles=["One More Time"])
633
+ get_filtered_content(mock_db, FilterArgs(exact_title=["One More Time"]))
633
634
  mock_query.by_title.assert_called_once_with("One More Time", exact=True)
634
635
 
635
636
  def test_album(self, mock_db, mock_query):
636
- get_filtered_content(mock_db, albums=["Discovery"])
637
+ get_filtered_content(mock_db, FilterArgs(album=["Discovery"]))
637
638
  mock_query.by_album.assert_called_once_with("Discovery")
638
639
 
639
640
  def test_exact_album(self, mock_db, mock_query):
640
- get_filtered_content(mock_db, exact_albums=["Discovery"])
641
+ get_filtered_content(mock_db, FilterArgs(exact_album=["Discovery"]))
641
642
  mock_query.by_album.assert_called_once_with("Discovery", exact=True)
642
643
 
643
644
  def test_playlist(self, mock_db, mock_query):
644
- get_filtered_content(mock_db, playlists=["My Playlist"])
645
+ get_filtered_content(mock_db, FilterArgs(playlist=["My Playlist"]))
645
646
  mock_query.by_playlist.assert_called_once_with("My Playlist")
646
647
 
647
648
  def test_exact_playlist(self, mock_db, mock_query):
648
- get_filtered_content(mock_db, exact_playlists=["My Playlist"])
649
+ get_filtered_content(mock_db, FilterArgs(exact_playlist=["My Playlist"]))
649
650
  mock_query.by_playlist.assert_called_once_with("My Playlist", exact=True)
650
651
 
651
652
  def test_path(self, mock_db, mock_query):
652
- get_filtered_content(mock_db, paths=["Music/track.mp3"])
653
+ get_filtered_content(mock_db, FilterArgs(path=["Music/track.mp3"]))
653
654
  mock_query.by_path.assert_called_once_with("Music/track.mp3")
654
655
 
655
656
  def test_exact_path(self, mock_db, mock_query):
656
- get_filtered_content(mock_db, exact_paths=["/Music/track.wav"])
657
+ get_filtered_content(mock_db, FilterArgs(exact_path=["/Music/track.wav"]))
657
658
  mock_query.by_path.assert_called_once_with("/Music/track.wav", exact=True)
658
659
 
659
660
  def test_format(self, mock_db, mock_query):
660
- get_filtered_content(mock_db, formats=["flac"])
661
+ get_filtered_content(mock_db, FilterArgs(format=["flac"]))
661
662
  mock_query.by_format.assert_called_once_with("flac")
662
663
 
663
664
  def test_multiple_formats(self, mock_db, mock_query):
664
- get_filtered_content(mock_db, formats=["flac", "aiff"])
665
+ get_filtered_content(mock_db, FilterArgs(format=["flac", "aiff"]))
665
666
  assert mock_query.by_format.call_count == 2
666
667
  mock_query.by_format.assert_any_call("flac")
667
668
  mock_query.by_format.assert_any_call("aiff")
668
669
 
669
670
  def test_match_all(self, mock_db, mock_query):
670
- get_filtered_content(mock_db, artists=["Daft Punk"], match_all=True)
671
+ get_filtered_content(mock_db, FilterArgs(artist=["Daft Punk"], match_all=True))
671
672
  mock_query.match_all.assert_called_once()
672
673
 
673
674
  def test_default_no_match_all(self, mock_db, mock_query):
674
- get_filtered_content(mock_db, artists=["Daft Punk"])
675
+ get_filtered_content(mock_db, FilterArgs(artist=["Daft Punk"]))
675
676
  mock_query.match_all.assert_not_called()
676
677
 
677
678
  def test_track_id_args_combined_with_format(self, mock_db, mock_query):
678
- """track_id_args should combine with other filters, not override them."""
679
+ """Positional track IDs should combine with other filters, not override them."""
679
680
  get_filtered_content(
680
- mock_db, track_id_args=["123"], formats=["flac"], match_all=True
681
+ mock_db,
682
+ FilterArgs(track_ids=["123"], format=["flac"], match_all=True),
681
683
  )
682
684
  mock_query.by_track_ids.assert_called_once_with(track_ids=["123"])
683
685
  mock_query.by_format.assert_called_once_with("flac")
@@ -686,7 +688,8 @@ class TestGetFilteredContent:
686
688
  def test_track_id_args_combined_with_artist(self, mock_db, mock_query):
687
689
  """Piped IDs + artist filter with match_all narrows to artist within that ID set."""
688
690
  get_filtered_content(
689
- mock_db, track_id_args=["123", "456"], artists=["Justice"], match_all=True
691
+ mock_db,
692
+ FilterArgs(track_ids=["123", "456"], artist=["Justice"], match_all=True),
690
693
  )
691
694
  mock_query.by_track_ids.assert_called_once_with(track_ids=["123", "456"])
692
695
  mock_query.by_artist.assert_called_once_with("Justice")
@@ -694,7 +697,7 @@ class TestGetFilteredContent:
694
697
 
695
698
  def test_track_id_args_or_with_artist(self, mock_db, mock_query):
696
699
  """Piped IDs OR'd with artist expands the result set."""
697
- get_filtered_content(mock_db, track_id_args=["123"], artists=["Justice"])
700
+ get_filtered_content(mock_db, FilterArgs(track_ids=["123"], artist=["Justice"]))
698
701
  mock_query.by_track_ids.assert_called_once_with(track_ids=["123"])
699
702
  mock_query.by_artist.assert_called_once_with("Justice")
700
703
  mock_query.match_all.assert_not_called()
@@ -704,7 +707,7 @@ class TestGetFilteredContent:
704
707
  mock_db.session = None
705
708
 
706
709
  with pytest.raises(RuntimeError, match="No Session"):
707
- get_filtered_content(mock_db)
710
+ get_filtered_content(mock_db, FilterArgs())
708
711
 
709
712
 
710
713
  class TestCollectionQueryExecution:
@@ -1010,7 +1010,7 @@ wheels = [
1010
1010
 
1011
1011
  [[package]]
1012
1012
  name = "rekordbox-edit"
1013
- version = "0.4.0.dev39"
1013
+ version = "0.4.0.dev40"
1014
1014
  source = { editable = "." }
1015
1015
  dependencies = [
1016
1016
  { name = "click" },