pyrekordbox 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

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 (71) hide show
  1. docs/source/formats/anlz.md +178 -7
  2. docs/source/formats/db6.md +1 -1
  3. docs/source/index.md +2 -6
  4. docs/source/quickstart.md +68 -45
  5. docs/source/tutorial/index.md +1 -1
  6. pyrekordbox/__init__.py +1 -1
  7. pyrekordbox/_version.py +2 -2
  8. pyrekordbox/anlz/file.py +39 -0
  9. pyrekordbox/anlz/structs.py +3 -5
  10. pyrekordbox/config.py +71 -27
  11. pyrekordbox/db6/database.py +260 -33
  12. pyrekordbox/db6/registry.py +22 -0
  13. pyrekordbox/db6/tables.py +3 -4
  14. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/METADATA +12 -11
  15. pyrekordbox-0.2.2.dist-info/RECORD +80 -0
  16. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/top_level.txt +0 -2
  17. tests/test_config.py +175 -0
  18. tests/test_db6.py +78 -0
  19. build/lib/build/lib/docs/source/conf.py +0 -178
  20. build/lib/build/lib/pyrekordbox/__init__.py +0 -22
  21. build/lib/build/lib/pyrekordbox/__main__.py +0 -204
  22. build/lib/build/lib/pyrekordbox/_version.py +0 -16
  23. build/lib/build/lib/pyrekordbox/anlz/__init__.py +0 -127
  24. build/lib/build/lib/pyrekordbox/anlz/file.py +0 -186
  25. build/lib/build/lib/pyrekordbox/anlz/structs.py +0 -299
  26. build/lib/build/lib/pyrekordbox/anlz/tags.py +0 -508
  27. build/lib/build/lib/pyrekordbox/config.py +0 -596
  28. build/lib/build/lib/pyrekordbox/db6/__init__.py +0 -45
  29. build/lib/build/lib/pyrekordbox/db6/aux_files.py +0 -213
  30. build/lib/build/lib/pyrekordbox/db6/database.py +0 -1808
  31. build/lib/build/lib/pyrekordbox/db6/registry.py +0 -304
  32. build/lib/build/lib/pyrekordbox/db6/tables.py +0 -1618
  33. build/lib/build/lib/pyrekordbox/logger.py +0 -23
  34. build/lib/build/lib/pyrekordbox/mysettings/__init__.py +0 -32
  35. build/lib/build/lib/pyrekordbox/mysettings/file.py +0 -369
  36. build/lib/build/lib/pyrekordbox/mysettings/structs.py +0 -282
  37. build/lib/build/lib/pyrekordbox/utils.py +0 -162
  38. build/lib/build/lib/pyrekordbox/xml.py +0 -1294
  39. build/lib/build/lib/tests/__init__.py +0 -3
  40. build/lib/build/lib/tests/test_anlz.py +0 -206
  41. build/lib/build/lib/tests/test_db6.py +0 -1039
  42. build/lib/build/lib/tests/test_mysetting.py +0 -203
  43. build/lib/build/lib/tests/test_xml.py +0 -629
  44. build/lib/docs/source/conf.py +0 -178
  45. build/lib/pyrekordbox/__init__.py +0 -22
  46. build/lib/pyrekordbox/__main__.py +0 -204
  47. build/lib/pyrekordbox/_version.py +0 -16
  48. build/lib/pyrekordbox/anlz/__init__.py +0 -127
  49. build/lib/pyrekordbox/anlz/file.py +0 -186
  50. build/lib/pyrekordbox/anlz/structs.py +0 -299
  51. build/lib/pyrekordbox/anlz/tags.py +0 -508
  52. build/lib/pyrekordbox/config.py +0 -596
  53. build/lib/pyrekordbox/db6/__init__.py +0 -45
  54. build/lib/pyrekordbox/db6/aux_files.py +0 -213
  55. build/lib/pyrekordbox/db6/database.py +0 -1808
  56. build/lib/pyrekordbox/db6/registry.py +0 -304
  57. build/lib/pyrekordbox/db6/tables.py +0 -1618
  58. build/lib/pyrekordbox/logger.py +0 -23
  59. build/lib/pyrekordbox/mysettings/__init__.py +0 -32
  60. build/lib/pyrekordbox/mysettings/file.py +0 -369
  61. build/lib/pyrekordbox/mysettings/structs.py +0 -282
  62. build/lib/pyrekordbox/utils.py +0 -162
  63. build/lib/pyrekordbox/xml.py +0 -1294
  64. build/lib/tests/__init__.py +0 -3
  65. build/lib/tests/test_anlz.py +0 -206
  66. build/lib/tests/test_db6.py +0 -1039
  67. build/lib/tests/test_mysetting.py +0 -203
  68. build/lib/tests/test_xml.py +0 -629
  69. pyrekordbox-0.2.1.dist-info/RECORD +0 -129
  70. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/LICENSE +0 -0
  71. {pyrekordbox-0.2.1.dist-info → pyrekordbox-0.2.2.dist-info}/WHEEL +0 -0
@@ -1,1808 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # Author: Dylan Jones
3
- # Date: 2023-08-13
4
-
5
- import logging
6
- import datetime
7
- import secrets
8
- from uuid import uuid4
9
- from pathlib import Path
10
- from typing import Optional
11
- from sqlalchemy import create_engine, or_, event, MetaData
12
- from sqlalchemy.orm import Session
13
- from sqlalchemy.exc import NoResultFound
14
- from sqlalchemy.sql.sqltypes import DateTime, String
15
- from packaging import version
16
- from ..utils import get_rekordbox_pid
17
- from ..config import get_config
18
- from ..anlz import get_anlz_paths, read_anlz_files
19
- from .registry import RekordboxAgentRegistry
20
- from .aux_files import MasterPlaylistXml
21
- from .tables import DjmdContent
22
- from . import tables
23
-
24
- try:
25
- from sqlcipher3 import dbapi2 as sqlite3 # noqa
26
- except ImportError:
27
- import sqlite3
28
-
29
- MAX_VERSION = version.parse("6.6.5")
30
-
31
- logger = logging.getLogger(__name__)
32
-
33
- rb6_config = get_config("rekordbox6")
34
-
35
-
36
- class IncompatibleVersionError(Exception):
37
- def __init__(self, rb_version):
38
- super().__init__(
39
- f"Incompatible rekordbox 6 version\n"
40
- f"Your are using rekordbox {rb_version} but the key extraction only works "
41
- f"for versions lower than {MAX_VERSION}.\n"
42
- "Please use the `key` parameter to manually provide the database key."
43
- )
44
-
45
-
46
- def open_rekordbox_database(path=None, key="", unlock=True, sql_driver=None):
47
- """Opens a connection to the Rekordbox v6 master.db SQLite3 database.
48
-
49
- Parameters
50
- ----------
51
- path : str or Path, optional
52
- The path of the Rekordbox v6 database file. By default, pyrekordbox
53
- automatically finds the Rekordbox v6 master.db database file.
54
- This parameter is only required for opening other databases or if the
55
- configuration fails.
56
- key : str, optional
57
- The database key. By default, pyrekordbox automatically reads the database
58
- key from the Rekordbox v6 configuration file. This parameter is only required
59
- if the key extraction fails.
60
- unlock: bool, optional
61
- Flag if the database needs to be decrypted. Set to False if you are opening
62
- an unencrypted test database.
63
- sql_driver : Callable, optional
64
- The SQLite driver to used for opening the database. The standard ``sqlite3``
65
- package is used as default driver.
66
-
67
- Returns
68
- -------
69
- con : sql_driver.Connection
70
- The opened Rekordbox v6 database connection.
71
-
72
- Examples
73
- --------
74
- Open the Rekordbox v6 master.db database:
75
-
76
- >>> db = open_rekordbox_database()
77
-
78
- Open a copy of the database:
79
-
80
- >>> db = open_rekordbox_database("path/to/master_copy.db")
81
-
82
- Open a decrypted copy of the database:
83
-
84
- >>> db = open_rekordbox_database("path/to/master_unlocked.db", unlock=False)
85
-
86
- To use the ``pysqlcipher3`` package as SQLite driver, either import it as
87
-
88
- >>> from sqlcipher3 import dbapi2 as sqlite3 # noqa
89
- >>> db = open_rekordbox_database("path/to/master_copy.db")
90
-
91
- or supply the package as driver:
92
-
93
- >>> from sqlcipher3 import dbapi2 # noqa
94
- >>> db = open_rekordbox_database("path/to/master_copy.db", sql_driver=dbapi2)
95
- """
96
- if not path:
97
- path = rb6_config["db_path"]
98
- path = Path(path)
99
- if not path.exists():
100
- raise FileNotFoundError(f"File '{path}' does not exist!")
101
- logger.info("Opening %s", path)
102
-
103
- # Open database
104
- if sql_driver is None:
105
- # Use default sqlite3 package
106
- # This requires that the 'sqlite3.dll' was replaced by the 'sqlcipher.dll'
107
- sql_driver = sqlite3
108
- con = sql_driver.connect(str(path))
109
-
110
- if unlock:
111
- if not key:
112
- ver = version.parse(rb6_config["version"])
113
- if ver >= MAX_VERSION:
114
- raise IncompatibleVersionError(rb6_config["version"])
115
- try:
116
- key = rb6_config["dp"]
117
- except KeyError:
118
- raise ValueError("Could not unlock database: No key found")
119
- logger.info("Key: %s", key)
120
- # Unlock database
121
- con.execute(f"PRAGMA key='{key}'")
122
-
123
- # Check connection
124
- try:
125
- con.execute("SELECT name FROM sqlite_master WHERE type='table';")
126
- except sqlite3.DatabaseError as e:
127
- msg = f"Opening database failed: '{e}'. Check if the database key is correct!"
128
- raise sqlite3.DatabaseError(msg)
129
- else:
130
- logger.info("Database unlocked!")
131
-
132
- return con
133
-
134
-
135
- def _parse_query_result(query, kwargs):
136
- if "ID" in kwargs or "registry_id" in kwargs:
137
- try:
138
- query = query.one()
139
- except NoResultFound:
140
- return None
141
- return query
142
-
143
-
144
- class Rekordbox6Database:
145
- """Rekordbox v6 master.db database handler.
146
-
147
- Parameters
148
- ----------
149
- path : str or Path, optional
150
- The path of the Rekordbox v6 database file. By default, pyrekordbox
151
- automatically finds the Rekordbox v6 master.db database file.
152
- This parameter is only required for opening other databases or if the
153
- configuration fails.
154
- db_dir: str, optional
155
- The path of the Rekordbox v6 database directory. By default, pyrekordbox
156
- automatically finds the Rekordbox v6 database directory. Usually this is also
157
- the root directory of the analysis files. This parameter is only required for
158
- finding the analysis root directory if you are opening a database, that is
159
- stored somewhere else.
160
- key : str, optional
161
- The database key. By default, pyrekordbox automatically reads the database
162
- key from the Rekordbox v6 configuration file. This parameter is only required
163
- if the key extraction fails.
164
- unlock: bool, optional
165
- Flag if the database needs to be decrypted. Set to False if you are opening
166
- an unencrypted test database.
167
-
168
- Attributes
169
- ----------
170
- engine : sqlalchemy.engine.Engine
171
- The SQLAlchemy engine instance for the Rekordbox v6 database.
172
- session : sqlalchemy.orm.Session
173
- The SQLAlchemy session instance bound to the engine.
174
-
175
- See Also
176
- --------
177
- pyrekordbox.db6.tables: Rekordbox v6 database table definitions
178
- create_rekordbox_engine: Creates the SQLAlchemy engine for the Rekordbox v6 database
179
-
180
- Examples
181
- --------
182
- Pyrekordbox automatically finds the Rekordbox v6 master.db database file and
183
- opens it when initializing the object:
184
-
185
- >>> db = Rekordbox6Database()
186
-
187
- Use the included getters for querying the database:
188
-
189
- >>> db.get_content()[0]
190
- <DjmdContent(40110712 Title=NOISE)>
191
- """
192
-
193
- def __init__(self, path=None, db_dir="", key="", unlock=True):
194
- pid = get_rekordbox_pid()
195
- if pid:
196
- logger.warning("Rekordbox is running!")
197
-
198
- if not path:
199
- # Get path from the RB config
200
- path = rb6_config.get("db_path", "")
201
- if not path:
202
- pdir = get_config("pioneer", "install_dir")
203
- raise FileNotFoundError(f"No Rekordbox v6 directory found in '{pdir}'")
204
- path = Path(path)
205
- # make sure file exists
206
- if not path.exists():
207
- raise FileNotFoundError(f"File '{path}' does not exist!")
208
- # Open database
209
- if unlock:
210
- if not key:
211
- ver = version.parse(rb6_config["version"])
212
- if ver >= MAX_VERSION:
213
- raise IncompatibleVersionError(rb6_config["version"])
214
- try:
215
- key = rb6_config["dp"]
216
- except KeyError:
217
- raise ValueError("Could not unlock database: No key found")
218
- logger.info("Key: %s", key)
219
- # Unlock database and create engine
220
- url = f"sqlite+pysqlcipher://:{key}@/{path}?"
221
- engine = create_engine(url, module=sqlite3)
222
- else:
223
- engine = create_engine(f"sqlite:///{path}")
224
-
225
- if not db_dir:
226
- db_dir = path.parent
227
- db_dir = Path(db_dir)
228
- if not db_dir.exists():
229
- raise FileNotFoundError(f"Database directory '{db_dir}' does not exist!")
230
-
231
- self.engine = engine
232
- self.session: Optional[Session] = None
233
-
234
- self.registry = RekordboxAgentRegistry(self)
235
- self._events = dict()
236
- try:
237
- self.playlist_xml = MasterPlaylistXml(db_dir=db_dir)
238
- except FileNotFoundError:
239
- logger.warning(f"No masterPlaylists6.xml found in {db_dir}")
240
- self.playlist_xml = None
241
-
242
- self._db_dir = db_dir
243
- self._share_dir = db_dir / "share"
244
-
245
- self.open()
246
-
247
- @property
248
- def no_autoflush(self):
249
- """Creates a no-autoflush context."""
250
- return self.session.no_autoflush
251
-
252
- @property
253
- def db_directory(self):
254
- return self._db_dir
255
-
256
- @property
257
- def share_directory(self):
258
- return self._share_dir
259
-
260
- def open(self):
261
- """Open the database by instantiating a new session using the SQLAchemy engine.
262
-
263
- A new session instance is only created if the session was closed previously.
264
-
265
- Examples
266
- --------
267
- >>> db = Rekordbox6Database()
268
- >>> db.close()
269
- >>> db.open()
270
- """
271
- if self.session is None:
272
- self.session = Session(bind=self.engine)
273
- self.registry.clear_buffer()
274
-
275
- def close(self):
276
- """Close the currently active session."""
277
- for key in self._events:
278
- self.unregister_event(key)
279
- self.registry.clear_buffer()
280
- self.session.close()
281
- self.session = None
282
-
283
- def __enter__(self):
284
- return self
285
-
286
- def __exit__(self, exc_type, exc_val, exc_tb):
287
- self.close()
288
-
289
- def register_event(self, identifier, fn):
290
- """Registers a session event callback.
291
-
292
- Parameters
293
- ----------
294
- identifier : str
295
- The identifier of the event, for example 'before_flush', 'after_commit', ...
296
- See the SQLAlchemy documentation for a list of valid event identifiers.
297
- fn : callable
298
- The event callback method.
299
- """
300
- event.listen(self.session, identifier, fn)
301
- self._events[identifier] = fn
302
-
303
- def unregister_event(self, identifier):
304
- """Removes an existing session event callback.
305
-
306
- Parameters
307
- ----------
308
- identifier : str
309
- The identifier of the event
310
- """
311
- fn = self._events[identifier]
312
- event.remove(self.session, identifier, fn)
313
-
314
- def query(self, *entities, **kwargs):
315
- """Creates a new SQL query for the given entities.
316
-
317
- Parameters
318
- ----------
319
- *entities : Base
320
- The table objects for which the query is created.
321
- **kwargs
322
- Arbitrary keyword arguments used for creating the query.
323
-
324
- Returns
325
- -------
326
- query : sqlalchemy.orm.query.Query
327
- The SQLAlchemy ``Query`` object.
328
-
329
- Examples
330
- --------
331
- Query the ``DjmdContent`` table
332
-
333
- >>> db = Rekordbox6Database()
334
- >>> query = db.query(DjmdContent)
335
-
336
- Query the `Title` attribute of the ``DjmdContent`` table
337
-
338
- >>> db = Rekordbox6Database()
339
- >>> query = db.query(DjmdContent.Title)
340
- """
341
- return self.session.query(*entities, **kwargs)
342
-
343
- def add(self, instance):
344
- """Add an element to the Rekordbox database.
345
-
346
- Parameters
347
- ----------
348
- instance : tables.Base
349
- The table entry to add.
350
- """
351
- self.session.add(instance)
352
- self.registry.on_create(instance)
353
-
354
- def delete(self, instance):
355
- """Delete an element from the Rekordbox database.
356
-
357
- Parameters
358
- ----------
359
- instance : tables.Base
360
- The table entry to delte.
361
- """
362
- self.session.delete(instance)
363
- self.registry.on_delete(instance)
364
-
365
- def get_local_usn(self):
366
- """Returns the local sequence number (update count) of Rekordbox.
367
-
368
- Any changes made to the `Djmd...` tables increments the local update count of
369
- Rekordbox. The ``usn`` entry of the changed row is set to the corresponding
370
- update count.
371
-
372
- Returns
373
- -------
374
- usn : int
375
- The value of the local update count.
376
- """
377
- return self.registry.get_local_update_count()
378
-
379
- def set_local_usn(self, usn):
380
- """Sets the local sequence number (update count) of Rekordbox.
381
-
382
- Parameters
383
- ----------
384
- usn : int or str
385
- The new update sequence number.
386
- """
387
- self.registry.set_local_update_count(usn)
388
-
389
- def increment_local_usn(self, num=1):
390
- """Increments the local update sequence number (update count) of Rekordbox.
391
-
392
- Parameters
393
- ----------
394
- num : int, optional
395
- The number of times to increment the update counter. By default, the counter
396
- is incremented by 1.
397
-
398
- Returns
399
- -------
400
- usn : int
401
- The value of the incremented local update count.
402
-
403
- Examples
404
- --------
405
- >>> db = Rekordbox6Database()
406
- >>> db.get_local_usn()
407
- 70500
408
-
409
- >>> db.increment_local_usn()
410
- 70501
411
-
412
- >>> db.get_local_usn()
413
- 70501
414
- """
415
- return self.registry.increment_local_update_count(num)
416
-
417
- def autoincrement_usn(self, set_row_usn=True):
418
- """Auto-increments the local USN for all uncommited changes.
419
-
420
- Parameters
421
- ----------
422
- set_row_usn : bool, optional
423
- If True, set the ``rb_local_usn`` value of updated or added rows according
424
- to the uncommited update sequence.
425
-
426
- Returns
427
- -------
428
- new_usn : int
429
- The new local update sequence number after applying all updates.
430
-
431
- Examples
432
- --------
433
- >>> db = Rekordbox6Database()
434
- >>> db.get_local_usn()
435
- 70500
436
-
437
- >>> content = db.get_content().first()
438
- >>> playlist = db.get_playlist().first()
439
- >>> content.Title = "New Title"
440
- >>> playlist.Name = "New Name"
441
- >>> db.autoincrement_usn(set_row_usn=True)
442
- >>> db.get_local_usn()
443
- 70502
444
- """
445
- return self.registry.autoincrement_local_update_count(set_row_usn)
446
-
447
- def flush(self):
448
- """Flushes the buffer of the SQLAlchemy session instance."""
449
- self.session.flush()
450
-
451
- def commit(self, autoinc=True):
452
- """Commit the changes made to the database.
453
-
454
- Parameters
455
- ----------
456
- autoinc : bool, optional
457
- If True, auto-increment the local and row USN's before commiting the
458
- changes made to the database.
459
-
460
- See Also
461
- --------
462
- autoincrement_usn : Auto-increments the local Rekordbox USN's.
463
- """
464
- pid = get_rekordbox_pid()
465
- if pid:
466
- raise RuntimeError(
467
- "Rekordbox is running. Please close Rekordbox before commiting changes."
468
- )
469
- if autoinc:
470
- self.registry.autoincrement_local_update_count(set_row_usn=True)
471
- self.session.commit()
472
- self.registry.clear_buffer()
473
-
474
- # Update the masterPlaylists6.xml file
475
- if self.playlist_xml is not None:
476
- # Sync the updated_at values of the playlists in the DB and the XML file
477
- for pl in self.get_playlist():
478
- plxml = self.playlist_xml.get(pl.ID)
479
- if plxml is None:
480
- raise ValueError(
481
- f"Playlist {pl.ID} not found in masterPlaylists6.xml! "
482
- "Did you add it manually? "
483
- "Use the create_playlist method instead."
484
- )
485
- ts = plxml["Timestamp"]
486
- diff = pl.updated_at - ts
487
- if abs(diff.total_seconds()) > 1:
488
- logger.debug("Updating updated_at of playlist %s in XML", pl.ID)
489
- self.playlist_xml.update(pl.ID, updated_at=pl.updated_at)
490
-
491
- # Save the XML file if it was modified
492
- if self.playlist_xml.modified:
493
- self.playlist_xml.save()
494
-
495
- def rollback(self):
496
- """Rolls back the uncommited changes to the database."""
497
- self.session.rollback()
498
- self.registry.clear_buffer()
499
-
500
- # -- Table queries -----------------------------------------------------------------
501
-
502
- def get_active_censor(self, **kwargs):
503
- """Creates a filtered query for the ``DjmdActiveCensor`` table."""
504
- query = self.query(tables.DjmdActiveCensor).filter_by(**kwargs)
505
- return _parse_query_result(query, kwargs)
506
-
507
- def get_album(self, **kwargs):
508
- """Creates a filtered query for the ``DjmdAlbum`` table."""
509
- query = self.query(tables.DjmdAlbum).filter_by(**kwargs)
510
- return _parse_query_result(query, kwargs)
511
-
512
- def get_artist(self, **kwargs):
513
- """Creates a filtered query for the ``DjmdArtist`` table."""
514
- query = self.query(tables.DjmdArtist).filter_by(**kwargs)
515
- return _parse_query_result(query, kwargs)
516
-
517
- def get_category(self, **kwargs):
518
- """Creates a filtered query for the ``DjmdCategory`` table."""
519
- query = self.query(tables.DjmdCategory).filter_by(**kwargs)
520
- return _parse_query_result(query, kwargs)
521
-
522
- def get_color(self, **kwargs):
523
- """Creates a filtered query for the ``DjmdColor`` table."""
524
- query = self.query(tables.DjmdColor).filter_by(**kwargs)
525
- return _parse_query_result(query, kwargs)
526
-
527
- def get_content(self, **kwargs):
528
- """Creates a filtered query for the ``DjmdContent`` table."""
529
- query = self.query(tables.DjmdContent).filter_by(**kwargs)
530
- return _parse_query_result(query, kwargs)
531
-
532
- # noinspection PyUnresolvedReferences
533
- def search_content(self, text):
534
- """Searches the contents of the ``DjmdContent`` table.
535
-
536
- The search is case-insensitive and includes the following collumns of the
537
- ``DjmdContent`` table:
538
-
539
- - `Album`
540
- - `Artist`
541
- - `Commnt`
542
- - `Composer`
543
- - `Genre`
544
- - `Key`
545
- - `OrgArtist`
546
- - `Remixer`
547
-
548
- Parameters
549
- ----------
550
- text : str
551
- The search text.
552
-
553
- Returns
554
- -------
555
- results : list[DjmdContent]
556
- The resulting content elements.
557
- """
558
- # Search standard columns
559
- query = self.query(tables.DjmdContent).filter(
560
- or_(
561
- DjmdContent.Title.contains(text),
562
- DjmdContent.Commnt.contains(text),
563
- DjmdContent.SearchStr.contains(text),
564
- )
565
- )
566
- results = set(query.all())
567
-
568
- # Search artist (Artist, OrgArtist, Composer and Remixer)
569
- artist_attrs = ["Artist", "OrgArtist", "Composer", "Remixer"]
570
- for attr in artist_attrs:
571
- query = self.query(DjmdContent).join(getattr(DjmdContent, attr))
572
- results.update(query.filter(tables.DjmdArtist.Name.contains(text)).all())
573
-
574
- # Search album
575
- query = self.query(DjmdContent).join(DjmdContent.Album)
576
- results.update(query.filter(tables.DjmdAlbum.Name.contains(text)).all())
577
-
578
- # Search Genre
579
- query = self.query(DjmdContent).join(DjmdContent.Genre)
580
- results.update(query.filter(tables.DjmdGenre.Name.contains(text)).all())
581
-
582
- # Search Key
583
- query = self.query(DjmdContent).join(DjmdContent.Key)
584
- results.update(query.filter(tables.DjmdKey.ScaleName.contains(text)).all())
585
-
586
- results = list(results)
587
- results.sort(key=lambda x: x.ID)
588
- return results
589
-
590
- def get_cue(self, **kwargs):
591
- """Creates a filtered query for the ``DjmdCue`` table."""
592
- query = self.query(tables.DjmdCue).filter_by(**kwargs)
593
- return _parse_query_result(query, kwargs)
594
-
595
- def get_device(self, **kwargs):
596
- """Creates a filtered query for the ``DjmdDevice`` table."""
597
- query = self.query(tables.DjmdDevice).filter_by(**kwargs)
598
- return _parse_query_result(query, kwargs)
599
-
600
- def get_genre(self, **kwargs):
601
- """Creates a filtered query for the ``DjmdGenre`` table."""
602
- query = self.query(tables.DjmdGenre).filter_by(**kwargs)
603
- return _parse_query_result(query, kwargs)
604
-
605
- def get_history(self, **kwargs):
606
- """Creates a filtered query for the ``DjmdHistory`` table."""
607
- query = self.query(tables.DjmdHistory).filter_by(**kwargs)
608
- return _parse_query_result(query, kwargs)
609
-
610
- def get_history_songs(self, **kwargs):
611
- """Creates a filtered query for the ``DjmdSongHistory`` table."""
612
- query = self.query(tables.DjmdSongHistory).filter_by(**kwargs)
613
- return _parse_query_result(query, kwargs)
614
-
615
- def get_hot_cue_banklist(self, **kwargs):
616
- """Creates a filtered query for the ``DjmdHotCueBanklist`` table."""
617
- query = self.query(tables.DjmdHotCueBanklist).filter_by(**kwargs)
618
- return _parse_query_result(query, kwargs)
619
-
620
- def get_hot_cue_banklist_songs(self, **kwargs):
621
- """Creates a filtered query for the ``DjmdSongHotCueBanklist`` table."""
622
- query = self.query(tables.DjmdSongHotCueBanklist).filter_by(**kwargs)
623
- return _parse_query_result(query, kwargs)
624
-
625
- def get_key(self, **kwargs):
626
- """Creates a filtered query for the ``DjmdKey`` table."""
627
- query = self.query(tables.DjmdKey).filter_by(**kwargs)
628
- return _parse_query_result(query, kwargs)
629
-
630
- def get_label(self, **kwargs):
631
- """Creates a filtered query for the ``DjmdLabel`` table."""
632
- query = self.query(tables.DjmdLabel).filter_by(**kwargs)
633
- return _parse_query_result(query, kwargs)
634
-
635
- def get_menu_items(self, **kwargs):
636
- """Creates a filtered query for the ``DjmdMenuItems`` table."""
637
- query = self.query(tables.DjmdMenuItems).filter_by(**kwargs)
638
- return _parse_query_result(query, kwargs)
639
-
640
- def get_mixer_param(self, **kwargs):
641
- """Creates a filtered query for the ``DjmdMixerParam`` table."""
642
- query = self.query(tables.DjmdMixerParam).filter_by(**kwargs)
643
- return _parse_query_result(query, kwargs)
644
-
645
- def get_my_tag(self, **kwargs):
646
- """Creates a filtered query for the ``DjmdMyTag`` table."""
647
- query = self.query(tables.DjmdMyTag).filter_by(**kwargs)
648
- return _parse_query_result(query, kwargs)
649
-
650
- def get_my_tag_songs(self, **kwargs):
651
- """Creates a filtered query for the ``DjmdSongMyTag`` table."""
652
- query = self.query(tables.DjmdSongMyTag).filter_by(**kwargs)
653
- return _parse_query_result(query, kwargs)
654
-
655
- def get_playlist(self, **kwargs):
656
- """Creates a filtered query for the ``DjmdPlaylist`` table."""
657
- query = self.query(tables.DjmdPlaylist).filter_by(**kwargs)
658
- return _parse_query_result(query, kwargs)
659
-
660
- def get_playlist_songs(self, **kwargs):
661
- """Creates a filtered query for the ``DjmdSongPlaylist`` table."""
662
- query = self.query(tables.DjmdSongPlaylist).filter_by(**kwargs)
663
- return _parse_query_result(query, kwargs)
664
-
665
- def get_property(self, **kwargs):
666
- """Creates a filtered query for the ``DjmdProperty`` table."""
667
- query = self.query(tables.DjmdProperty).filter_by(**kwargs)
668
- return _parse_query_result(query, kwargs)
669
-
670
- def get_related_tracks(self, **kwargs):
671
- """Creates a filtered query for the ``DjmdRelatedTracks`` table."""
672
- query = self.query(tables.DjmdRelatedTracks).filter_by(**kwargs)
673
- return _parse_query_result(query, kwargs)
674
-
675
- def get_related_tracks_songs(self, **kwargs):
676
- """Creates a filtered query for the ``DjmdSongRelatedTracks`` table."""
677
- query = self.query(tables.DjmdSongRelatedTracks).filter_by(**kwargs)
678
- return _parse_query_result(query, kwargs)
679
-
680
- def get_sampler(self, **kwargs):
681
- """Creates a filtered query for the ``DjmdSampler`` table."""
682
- query = self.query(tables.DjmdSampler).filter_by(**kwargs)
683
- return _parse_query_result(query, kwargs)
684
-
685
- def get_sampler_songs(self, **kwargs):
686
- """Creates a filtered query for the ``DjmdSongSampler`` table."""
687
- query = self.query(tables.DjmdSongSampler).filter_by(**kwargs)
688
- return _parse_query_result(query, kwargs)
689
-
690
- def get_tag_list_songs(self, **kwargs):
691
- """Creates a filtered query for the ``DjmdSongTagList`` table."""
692
- query = self.query(tables.DjmdSongTagList).filter_by(**kwargs)
693
- return _parse_query_result(query, kwargs)
694
-
695
- def get_sort(self, **kwargs):
696
- """Creates a filtered query for the ``DjmdSort`` table."""
697
- query = self.query(tables.DjmdSort).filter_by(**kwargs)
698
- return _parse_query_result(query, kwargs)
699
-
700
- def get_agent_registry(self, **kwargs):
701
- """Creates a filtered query for the ``AgentRegistry`` table."""
702
- query = self.query(tables.AgentRegistry).filter_by(**kwargs)
703
- return _parse_query_result(query, kwargs)
704
-
705
- def get_cloud_agent_registry(self, **kwargs):
706
- """Creates a filtered query for the ``CloudAgentRegistry`` table."""
707
- query = self.query(tables.CloudAgentRegistry).filter_by(**kwargs)
708
- return _parse_query_result(query, kwargs)
709
-
710
- def get_content_active_censor(self, **kwargs):
711
- """Creates a filtered query for the ``ContentActiveCensor`` table."""
712
- query = self.query(tables.ContentActiveCensor).filter_by(**kwargs)
713
- return _parse_query_result(query, kwargs)
714
-
715
- def get_content_cue(self, **kwargs):
716
- """Creates a filtered query for the ``ContentCue`` table."""
717
- query = self.query(tables.ContentCue).filter_by(**kwargs)
718
- return _parse_query_result(query, kwargs)
719
-
720
- def get_content_file(self, **kwargs):
721
- """Creates a filtered query for the ``ContentFile`` table."""
722
- query = self.query(tables.ContentFile).filter_by(**kwargs)
723
- return _parse_query_result(query, kwargs)
724
-
725
- def get_hot_cue_banklist_cue(self, **kwargs):
726
- """Creates a filtered query for the ``HotCueBanklistCue`` table."""
727
- query = self.query(tables.HotCueBanklistCue).filter_by(**kwargs)
728
- return _parse_query_result(query, kwargs)
729
-
730
- def get_image_file(self, **kwargs):
731
- """Creates a filtered query for the ``ImageFile`` table."""
732
- query = self.query(tables.ImageFile).filter_by(**kwargs)
733
- return _parse_query_result(query, kwargs)
734
-
735
- def get_setting_file(self, **kwargs):
736
- """Creates a filtered query for the ``SettingFile`` table."""
737
- query = self.query(tables.SettingFile).filter_by(**kwargs)
738
- return _parse_query_result(query, kwargs)
739
-
740
- def get_uuid_map(self, **kwargs):
741
- """Creates a filtered query for the ``UuidIDMap`` table."""
742
- query = self.query(tables.UuidIDMap).filter_by(**kwargs)
743
- return _parse_query_result(query, kwargs)
744
-
745
- # -- Database updates --------------------------------------------------------------
746
-
747
- def generate_unused_id(self, table, is_28_bit: bool = True) -> int:
748
- """Generates an unused ID for the given table."""
749
- max_tries = 1000000
750
- for _ in range(max_tries):
751
- # Generate random ID
752
- buf = secrets.token_bytes(4)
753
- id_ = (buf[0] << 24) + (buf[1] << 16) + (buf[2] << 8) + buf[3] >> 0
754
- if is_28_bit:
755
- id_ = id_ >> 4
756
- if id_ < 100:
757
- continue
758
- # Check if ID is already used
759
- query = self.query(table.ID).filter(table.ID == id_)
760
- used = self.query(query.exists()).scalar()
761
- if not used:
762
- return id_
763
-
764
- raise ValueError("Could not generate unused ID")
765
-
766
- def add_to_playlist(self, playlist, content, track_no=None):
767
- """Adds a track to a playlist.
768
-
769
- Creates a new :class:`DjmdSongPlaylist` object corresponding to the given
770
- content and adds it to the playlist.
771
-
772
- Parameters
773
- ----------
774
- playlist : DjmdPlaylist or int or str
775
- The playlist to add the track to. Can either be a :class:`DjmdPlaylist`
776
- object or a playlist ID.
777
- content : DjmdContent or int or str
778
- The content to add to the playlist. Can either be a :class:`DjmdContent`
779
- object or a content ID.
780
- track_no : int, optional
781
- The track number to add the content to. If not specified, the track
782
- will be added to the end of the playlist.
783
-
784
- Returns
785
- -------
786
- song: DjmdSongPlaylist
787
- The song playlist object that was created from the content.
788
-
789
- Raises
790
- ------
791
- ValueError : If the playlist is a folder or smart playlist.
792
- ValueError : If the track number is less than 1 or to large.
793
-
794
- Examples
795
- --------
796
- Add a track to the end of a playlist:
797
-
798
- >>> db = Rekordbox6Database()
799
- >>> cid = 12345 # Content ID
800
- >>> pid = 56789 # Playlist ID
801
- >>> db.add_to_playlist(pid, cid)
802
- <DjmdSongPlaylist(c803dfde-2236-4659-b3d7-e57221663375)>
803
-
804
- Add a track to the beginning of a playlist:
805
-
806
- >>> new_song = db.add_to_playlist(pid, cid, track_no=1)
807
- >>> new_song.TrackNo
808
- 1
809
- """
810
- if isinstance(playlist, (int, str)):
811
- playlist = self.get_playlist(ID=playlist)
812
- if isinstance(content, (int, str)):
813
- content = self.get_content(ID=content)
814
- # Check playlist attribute (can't be folder or smart playlist)
815
- if playlist.Attribute != 0:
816
- raise ValueError("Playlist must be a normal playlist")
817
-
818
- uuid = str(uuid4())
819
- id_ = str(uuid4())
820
- now = datetime.datetime.now()
821
- nsongs = (
822
- self.query(tables.DjmdSongPlaylist)
823
- .filter_by(PlaylistID=playlist.ID)
824
- .count()
825
- )
826
- if track_no is not None:
827
- insert_at_end = False
828
- track_no = int(track_no)
829
- if track_no < 1:
830
- raise ValueError("Track number must be greater than 0")
831
- if track_no > nsongs + 1:
832
- raise ValueError(
833
- f"Track number too high, parent contains {nsongs} items"
834
- )
835
- else:
836
- insert_at_end = True
837
- track_no = nsongs + 1
838
-
839
- cid = content.ID
840
- pid = playlist.ID
841
-
842
- logger.info("Adding content with ID=%s to playlist with ID=%s:", cid, pid)
843
- logger.debug("Content ID: %s", cid)
844
- logger.debug("Playlist ID: %s", pid)
845
- logger.debug("ID: %s", id_)
846
- logger.debug("UUID: %s", uuid)
847
- logger.debug("TrackNo: %s", track_no)
848
-
849
- moved = list()
850
- if not insert_at_end:
851
- self.registry.disable_tracking()
852
- # Update track numbers higher than the removed track
853
- query = (
854
- self.query(tables.DjmdSongPlaylist)
855
- .filter(
856
- tables.DjmdSongPlaylist.PlaylistID == playlist.ID,
857
- tables.DjmdSongPlaylist.TrackNo >= track_no,
858
- )
859
- .order_by(tables.DjmdSongPlaylist.TrackNo)
860
- )
861
- for song in query:
862
- song.TrackNo += 1
863
- song.updated_at = now
864
- moved.append(song)
865
- self.registry.enable_tracking()
866
-
867
- # Add song to playlist
868
- song = tables.DjmdSongPlaylist.create(
869
- ID=id_,
870
- PlaylistID=str(pid),
871
- ContentID=str(cid),
872
- TrackNo=track_no,
873
- UUID=uuid,
874
- created_at=now,
875
- updated_at=now,
876
- )
877
- self.add(song)
878
- if not insert_at_end:
879
- moved.append(song)
880
- self.registry.on_move(moved)
881
-
882
- return song
883
-
884
- def remove_from_playlist(self, playlist, song):
885
- """Removes a track from a playlist.
886
-
887
- Parameters
888
- ----------
889
- playlist : DjmdPlaylist or int or str
890
- The playlist to remove the track from. Can either be a :class:`DjmdPlaylist`
891
- object or a playlist ID.
892
- song : DjmdSongPlaylist or int or str
893
- The song to remove from the playlist. Can either be a
894
- :class:`DjmdSongPlaylist` object or a song ID.
895
-
896
- Examples
897
- --------
898
- Remove a track from a playlist:
899
-
900
- >>> db = Rekordbox6Database()
901
- >>> pid = 56789
902
- >>> pl = db.get_playlist(ID=pid)
903
- >>> song = pl.Songs[0]
904
- >>> db.remove_from_playlist(pl, song)
905
- """
906
- if isinstance(playlist, (int, str)):
907
- playlist = self.get_playlist(ID=playlist)
908
- if isinstance(song, (int, str)):
909
- song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
910
- logger.info(
911
- "Removing song with ID=%s from playlist with ID=%s", song.ID, playlist.ID
912
- )
913
- now = datetime.datetime.now()
914
- # Remove track from playlist
915
- track_no = song.TrackNo
916
- self.delete(song)
917
- self.commit()
918
- # Update track numbers higher than the removed track
919
- query = (
920
- self.query(tables.DjmdSongPlaylist)
921
- .filter(
922
- tables.DjmdSongPlaylist.PlaylistID == playlist.ID,
923
- tables.DjmdSongPlaylist.TrackNo > track_no,
924
- )
925
- .order_by(tables.DjmdSongPlaylist.TrackNo)
926
- )
927
- moved = list()
928
- self.registry.disable_tracking()
929
- for song in query:
930
- song.TrackNo -= 1
931
- song.updated_at = now
932
- moved.append(song)
933
- self.registry.enable_tracking()
934
- if moved:
935
- self.registry.on_move(moved)
936
-
937
- def move_song_in_playlist(self, playlist, song, new_track_no):
938
- """Sets a new track number of a song.
939
-
940
- Also updates the track numbers of the other songs in the playlist.
941
-
942
- Parameters
943
- ----------
944
- playlist : DjmdPlaylist or int or str
945
- The playlist the track is in. Can either be a :class:`DjmdPlaylist`
946
- object or a playlist ID.
947
- song : DjmdSongPlaylist or int or str
948
- The song to move inside the playlist. Can either be a
949
- :class:`DjmdSongPlaylist` object or a song ID.
950
- new_track_no : int
951
- The new track number of the song. Must be greater than 0 and less than
952
- the number of songs in the playlist.
953
-
954
- Examples
955
- --------
956
- Take a playlist containing a few tracks:
957
-
958
- >>> db = Rekordbox6Database()
959
- >>> pid = 56789
960
- >>> pl = db.get_playlist(ID=pid)
961
- >>> songs = sorted(pl.Songs, key=lambda x: x.TrackNo)
962
- >>> [s.Content.Title for s in songs] # noqa
963
- ['Demo Track 1', 'Demo Track 2', 'HORN', 'NOISE']
964
-
965
- Move a track forward in a playlist:
966
-
967
- >>> song = songs[2]
968
- >>> db.move_song_in_playlist(pl, song, new_track_no=1)
969
- >>> [s.Content.Title for s in sorted(pl.Songs, key=lambda x: x.TrackNo)] # noqa
970
- ['HORN', 'Demo Track 1', 'Demo Track 2', 'NOISE']
971
-
972
- Move a track backward in a playlist:
973
-
974
- >>> song = songs[1]
975
- >>> db.move_song_in_playlist(pl, song, new_track_no=4)
976
- >>> [s.Content.Title for s in sorted(pl.Songs, key=lambda x: x.TrackNo)] # noqa
977
- ['Demo Track 1', 'HORN', 'NOISE', 'Demo Track 2']
978
- """
979
- if isinstance(playlist, (int, str)):
980
- playlist = self.get_playlist(ID=playlist)
981
- if isinstance(song, (int, str)):
982
- song = self.query(tables.DjmdSongPlaylist).filter_by(ID=song).one()
983
- nsongs = (
984
- self.query(tables.DjmdSongPlaylist)
985
- .filter_by(PlaylistID=playlist.ID)
986
- .count()
987
- )
988
- if new_track_no < 1:
989
- raise ValueError("Track number must be greater than 0")
990
- if new_track_no > nsongs + 1:
991
- raise ValueError(f"Track number too high, parent contains {nsongs} items")
992
- logger.info(
993
- "Moving song with ID=%s in playlist with ID=%s to %s",
994
- song.ID,
995
- playlist.ID,
996
- new_track_no,
997
- )
998
- now = datetime.datetime.now()
999
- old_track_no = song.TrackNo
1000
-
1001
- self.registry.disable_tracking()
1002
- moved = list()
1003
- if new_track_no > old_track_no:
1004
- query = (
1005
- self.query(tables.DjmdSongPlaylist)
1006
- .filter(
1007
- tables.DjmdSongPlaylist.PlaylistID == playlist.ID,
1008
- old_track_no < tables.DjmdSongPlaylist.TrackNo,
1009
- tables.DjmdSongPlaylist.TrackNo <= new_track_no,
1010
- )
1011
- .order_by(tables.DjmdSongPlaylist.TrackNo)
1012
- )
1013
- for other_song in query:
1014
- other_song.TrackNo -= 1
1015
- other_song.updated_at = now
1016
- moved.append(other_song)
1017
- elif new_track_no < old_track_no:
1018
- query = self.query(tables.DjmdSongPlaylist).filter(
1019
- tables.DjmdSongPlaylist.PlaylistID == playlist.ID,
1020
- new_track_no <= tables.DjmdSongPlaylist.TrackNo,
1021
- tables.DjmdSongPlaylist.TrackNo < old_track_no,
1022
- )
1023
- for other_song in query:
1024
- other_song.TrackNo += 1
1025
- other_song.updated_at = now
1026
- moved.append(other_song)
1027
- else:
1028
- return
1029
-
1030
- song.TrackNo = new_track_no
1031
- song.updated_at = now
1032
- moved.append(song)
1033
-
1034
- self.registry.enable_tracking()
1035
- self.registry.on_move(moved)
1036
-
1037
- def _create_playlist(
1038
- self, name, seq, image_path, parent, smart_list=None, attribute=None
1039
- ):
1040
- """Creates a new playlist object."""
1041
- table = tables.DjmdPlaylist
1042
- id_ = str(self.generate_unused_id(table, is_28_bit=True))
1043
- uuid = str(uuid4())
1044
- now = datetime.datetime.now()
1045
-
1046
- if parent is None:
1047
- # If no parent is given, use root playlist
1048
- parent_id = "root"
1049
- elif isinstance(parent, tables.DjmdPlaylist):
1050
- # Check if parent is a folder
1051
- parent_id = parent.ID
1052
- if parent.Attribute != 1:
1053
- raise ValueError("Parent is not a folder")
1054
- else:
1055
- # Check if parent exists and is a folder
1056
- parent_id = parent
1057
- query = self.query(table.ID).filter(
1058
- table.ID == parent_id, table.Attribute == 1
1059
- )
1060
- if not self.query(query.exists()).scalar():
1061
- raise ValueError("Parent does not exist or is not a folder")
1062
-
1063
- n = self.get_playlist(ParentID=parent_id).count()
1064
- logger.debug("Parent playlist with ID=%s contains %s items", parent_id, n)
1065
-
1066
- if seq is None:
1067
- # New playlist is last in parents
1068
- seq = n + 1
1069
- insert_at_end = True
1070
- else:
1071
- # Check if sequence number is valid
1072
- insert_at_end = False
1073
- if seq < 1:
1074
- raise ValueError("Sequence number must be greater than 0")
1075
- elif seq > n + 1:
1076
- raise ValueError(f"Sequence number too high, parent contains {n} items")
1077
-
1078
- logger.debug("ID: %s", id_)
1079
- logger.debug("UUID: %s", uuid)
1080
- logger.debug("Name: %s", name)
1081
- logger.debug("Parent ID: %s", parent_id)
1082
- logger.debug("Seq: %s", seq)
1083
- logger.debug("Attribute: %s", attribute)
1084
- logger.debug("Smart List: %s", smart_list)
1085
- logger.debug("Image Path: %s", image_path)
1086
-
1087
- # Update seq numbers higher than the new seq number
1088
- if not insert_at_end:
1089
- query = self.query(tables.DjmdPlaylist).filter(
1090
- tables.DjmdPlaylist.ParentID == parent_id,
1091
- tables.DjmdPlaylist.Seq >= seq,
1092
- )
1093
- for pl in query:
1094
- pl.Seq += 1
1095
- self.registry.disable_tracking()
1096
- pl.updated_at = now
1097
- self.registry.enable_tracking()
1098
-
1099
- # Add new playlist to database
1100
- # First create with name 'New playlist'
1101
- playlist = table.create(
1102
- ID=id_,
1103
- Seq=seq,
1104
- Name="New playlist",
1105
- ImagePath=image_path,
1106
- Attribute=attribute,
1107
- ParentID=parent_id,
1108
- SmartList=smart_list,
1109
- UUID=uuid,
1110
- created_at=now,
1111
- updated_at=now,
1112
- )
1113
- self.add(playlist)
1114
- # Then update with correct name for correct USN
1115
- playlist.Name = name
1116
-
1117
- # Update masterPlaylists6.xml
1118
- if self.playlist_xml is not None:
1119
- self.playlist_xml.add(
1120
- id_, parent_id, attribute, now, lib_type=0, check_type=0
1121
- )
1122
-
1123
- return playlist
1124
-
1125
- def create_playlist(self, name, parent=None, seq=None, image_path=None):
1126
- """Creates a new playlist in the database.
1127
-
1128
- Parameters
1129
- ----------
1130
- name : str
1131
- The name of the new playlist.
1132
- parent : DjmdPlaylist or int or str, optional
1133
- The parent playlist of the new playlist. If not given, the playlist will be
1134
- added to the root playlist. Can either be a :class:`DjmdPlaylist` object or
1135
- a playlist ID.
1136
- seq : int, optional
1137
- The sequence number of the new playlist. If not given, the playlist will be
1138
- added at the end of the parent playlist.
1139
- image_path : str, optional
1140
- The path to the image file of the new playlist.
1141
-
1142
- Returns
1143
- -------
1144
- playlist : DjmdPlaylist
1145
- The newly created playlist.
1146
-
1147
- Raises
1148
- ------
1149
- ValueError : If the parent playlist is not a folder.
1150
- ValueError : If the sequence number is less than 1 or to large.
1151
-
1152
- Examples
1153
- --------
1154
- Create a new playlist in the root playlist:
1155
-
1156
- >>> db = Rekordbox6Database()
1157
- >>> pl = db.create_playlist("My Playlist")
1158
- >>> pl.ParentID
1159
- 'root'
1160
-
1161
- Create a new playlist in a folder:
1162
-
1163
- >>> folder = db.get_playlist(Name="My Folder").one()
1164
- >>> pl = db.create_playlist("My Playlist", parent=folder)
1165
- >>> pl.ParentID
1166
- '123456'
1167
- """
1168
- logger.info("Creating playlist %s", name)
1169
- return self._create_playlist(name, seq, image_path, parent, attribute=0)
1170
-
1171
- def create_playlist_folder(self, name, parent=None, seq=None, image_path=None):
1172
- """Creates a new playlist folder in the database.
1173
-
1174
- Parameters
1175
- ----------
1176
- name : str
1177
- The name of the new playlist folder.
1178
- parent : DjmdPlaylist or int or str, optional
1179
- The parent playlist of the new folder. If not given, the playlist will be
1180
- added to the root playlist. Can either be a :class:`DjmdPlaylist` object or
1181
- a playlist ID.
1182
- seq : int, optional
1183
- The sequence number of the new folder. If not given, the playlist will be
1184
- added at the end of the parent playlist.
1185
- image_path : str, optional
1186
- The path to the image file of the new playlist.
1187
-
1188
- Returns
1189
- -------
1190
- playlist_folder : DjmdPlaylist
1191
- The newly created playlist folder.
1192
-
1193
- Examples
1194
- --------
1195
- Create a new playlist folder in the root playlist:
1196
-
1197
- >>> db = Rekordbox6Database()
1198
- >>> folder1 = db.create_playlist_folder("My Playlist Folder")
1199
- >>> folder1.ParentID
1200
- 'root'
1201
-
1202
- Create a new playlist folder in the other folder:
1203
-
1204
- >>> folder2 = db.create_playlist("My Playlist Folder2", parent=folder1)
1205
- >>> folder2.ParentID
1206
- '123456'
1207
- """
1208
- logger.info("Creating playlist folder %s", name)
1209
- return self._create_playlist(name, seq, image_path, parent, attribute=1)
1210
-
1211
- def delete_playlist(self, playlist):
1212
- """Deletes a playlist or playlist folder from the database.
1213
-
1214
- Parameters
1215
- ----------
1216
- playlist : DjmdPlaylist or int or str
1217
- The playlist or playlist folder to delete. Can either be a
1218
- :class:`DjmdPlaylist` object or a playlist ID.
1219
-
1220
- Examples
1221
- --------
1222
- Delete a playlist:
1223
-
1224
- >>> db = Rekordbox6Database()
1225
- >>> pl = db.get_playlist(Name="My Playlist").one()
1226
- >>> db.delete_playlist(pl)
1227
-
1228
- Delete a playlist folder:
1229
-
1230
- >>> folder = db.get_playlist(Name="My Folder").one()
1231
- >>> db.delete_playlist(folder)
1232
- """
1233
- if isinstance(playlist, (int, str)):
1234
- playlist = self.get_playlist(ID=playlist)
1235
-
1236
- if playlist.Attribute == 1:
1237
- logger.info(
1238
- "Deleting playlist folder '%s' with ID=%s", playlist.Name, playlist.ID
1239
- )
1240
- else:
1241
- logger.info("Deleting playlist '%s' with ID=%s", playlist.Name, playlist.ID)
1242
-
1243
- now = datetime.datetime.now()
1244
- seq = playlist.Seq
1245
- parent_id = playlist.ParentID
1246
-
1247
- self.registry.disable_tracking()
1248
- # Update seq numbers higher than the deleted seq number
1249
- query = (
1250
- self.query(tables.DjmdPlaylist)
1251
- .filter(
1252
- tables.DjmdPlaylist.ParentID == parent_id,
1253
- tables.DjmdPlaylist.Seq > seq,
1254
- )
1255
- .order_by(tables.DjmdPlaylist.Seq)
1256
- )
1257
- moved = list()
1258
- for pl in query:
1259
- pl.Seq -= 1
1260
- pl.updated_at = now
1261
- moved.append(pl)
1262
- moved.append(playlist)
1263
-
1264
- children = [playlist]
1265
- # Get all child playlist IDs
1266
- child_ids = list()
1267
- while len(children):
1268
- new_children = list()
1269
- for child in children:
1270
- child_ids.append(child.ID)
1271
- new_children.extend(list(child.Children))
1272
- children = new_children
1273
-
1274
- # First ID in 'child_ids' is always the deleted playlist, others are children
1275
-
1276
- # Remove playlist from masterPlaylists6.xml
1277
- if self.playlist_xml is not None:
1278
- for pid in child_ids:
1279
- self.playlist_xml.remove(pid)
1280
-
1281
- # Remove playlist from database
1282
- self.delete(playlist)
1283
- self.registry.enable_tracking()
1284
- if len(child_ids) > 1:
1285
- # The playlist folder had children: on extra USN increment
1286
- self.registry.on_delete(child_ids[1:])
1287
- self.registry.on_delete(moved)
1288
-
1289
- def move_playlist(self, playlist, parent=None, seq=None):
1290
- """Moves a playlist (folder) in the current parent folder or to a new one.
1291
-
1292
- Parameters
1293
- ----------
1294
- playlist : DjmdPlaylist or int or str
1295
- The playlist or playlist folder to move. Can either be a
1296
- :class:`DjmdPlaylist` object or a playlist ID.
1297
- parent : DjmdPlaylist or int or str, optional
1298
- The new parent playlist of the playlist. If not given, the playlist will
1299
- be moved to `seq` in the current parent playlist. Can either be a
1300
- :class:`DjmdPlaylist` object or a playlist ID.
1301
- seq : int, optional
1302
- The new sequence number of the playlist. If the `parent` argument is given,
1303
- the playlist will be moved to `seq` in the new parent playlist or to
1304
- the end of the new parent folder if `seq=None`. If the `parent` argument is
1305
- not given, the playlist will be moved to `seq` in the current parent.
1306
-
1307
- Examples
1308
- --------
1309
- Take the following playlist tree:
1310
-
1311
- >>> db = Rekordbox6Database()
1312
- >>> playlists = db.get_playlist().order_by(tables.DjmdPlaylist.Seq)
1313
- >>> [pl.Name for pl in playlists] # noqa
1314
- ['Folder 1', 'Folder 2', 'Playlist 1', 'Playlist 2', 'Playlist 3']
1315
-
1316
- The playlists and folders above are all in the `root` plalyist folder.
1317
- Move a playlist in the current parent folder:
1318
-
1319
- >>> pl = db.get_playlist(Name="Playlist 2").one() # noqa
1320
- >>> db.move_playlist(pl, seq=2)
1321
- >>> playlists = db.get_playlist().order_by(tables.DjmdPlaylist.Seq)
1322
- >>> [pl.Name for pl in playlists] # noqa
1323
- ['Folder 1', 'Playlist 2', 'Folder 2', 'Playlist 1', 'Playlist 3']
1324
-
1325
- Move a playlist to a new parent folder:
1326
-
1327
- >>> pl = db.get_playlist(Name="Playlist 1").one() # noqa
1328
- >>> parent = db.get_playlist(Name="Folder 1").one() # noqa
1329
- >>> db.move_playlist(pl, parent=parent)
1330
- >>> db.get_playlist(ParentID=parent.ID).all()
1331
- ['Playlist 1']
1332
- """
1333
- if parent is None and seq is None:
1334
- raise ValueError("Either parent or seq must be given")
1335
- if isinstance(playlist, (int, str)):
1336
- playlist = self.get_playlist(ID=playlist)
1337
-
1338
- now = datetime.datetime.now()
1339
- table = tables.DjmdPlaylist
1340
-
1341
- if parent is None:
1342
- # If no parent is given, keep the current parent
1343
- parent_id = playlist.ParentID
1344
- elif isinstance(parent, tables.DjmdPlaylist):
1345
- # Check if parent is a folder
1346
- parent_id = parent.ID
1347
- if parent.Attribute != 1:
1348
- raise ValueError("Parent is not a folder")
1349
- else:
1350
- # Check if parent exists and is a folder
1351
- parent_id = str(parent)
1352
- query = self.query(table.ID).filter(
1353
- table.ID == parent_id, table.Attribute == 1
1354
- )
1355
- if not self.query(query.exists()).scalar():
1356
- raise ValueError("Parent does not exist or is not a folder")
1357
-
1358
- n = self.get_playlist(ParentID=parent_id).count()
1359
- old_seq = playlist.Seq
1360
-
1361
- if parent_id != playlist.ParentID:
1362
- # Move to new parent
1363
-
1364
- old_parent_id = playlist.ParentID
1365
- if seq is None:
1366
- # New playlist is last in parents
1367
- seq = n + 1
1368
- insert_at_end = True
1369
- else:
1370
- # Check if sequence number is valid
1371
- insert_at_end = False
1372
- if seq < 1:
1373
- raise ValueError("Sequence number must be greater than 0")
1374
- elif seq > n + 1:
1375
- raise ValueError(
1376
- f"Sequence number too high, parent contains {n} items"
1377
- )
1378
-
1379
- if not insert_at_end:
1380
- # Get all playlists with seq between old_seq and seq
1381
- query = (
1382
- self.query(tables.DjmdPlaylist)
1383
- .filter(
1384
- tables.DjmdPlaylist.ParentID == parent_id,
1385
- tables.DjmdPlaylist.Seq >= seq,
1386
- )
1387
- .order_by(tables.DjmdPlaylist.Seq)
1388
- )
1389
- other_playlists = query.all()
1390
- # Set seq number and update time *before* other playlists to ensure
1391
- # right USN increment order
1392
- playlist.ParentID = parent_id
1393
- self.registry.disable_tracking()
1394
- playlist.Seq = seq
1395
- playlist.updated_at = now
1396
- self.registry.enable_tracking()
1397
-
1398
- if not insert_at_end:
1399
- # Update seq numbers higher than the new seq number in *new* parent
1400
- # noinspection PyUnboundLocalVariable
1401
- for pl in other_playlists:
1402
- # Update time of other playlists are left unchanged
1403
- pl.Seq += 1
1404
- # Each move counts as one USN increment, so disable for update time
1405
- self.registry.disable_tracking()
1406
- pl.updated_at = now
1407
- self.registry.enable_tracking()
1408
-
1409
- # Update seq numbers higher than the old seq number in *old* parent
1410
- # USN is not updated here
1411
- self.registry.disable_tracking()
1412
- query = (
1413
- self.query(tables.DjmdPlaylist)
1414
- .filter(
1415
- tables.DjmdPlaylist.ParentID == old_parent_id,
1416
- tables.DjmdPlaylist.Seq > old_seq,
1417
- )
1418
- .order_by(tables.DjmdPlaylist.Seq)
1419
- )
1420
- for pl in query:
1421
- # Update time of other playlists are left unchanged
1422
- pl.Seq -= 1
1423
- pl.updated_at = now
1424
- self.registry.enable_tracking()
1425
-
1426
- else:
1427
- # Keep parent, only change seq number
1428
-
1429
- if seq < 1:
1430
- raise ValueError("Sequence number must be greater than 0")
1431
- elif seq > n + 1:
1432
- raise ValueError(f"Sequence number too high, parent contains {n} items")
1433
-
1434
- if seq > old_seq:
1435
- # Get all playlists with seq between old_seq and seq
1436
- query = (
1437
- self.query(tables.DjmdPlaylist)
1438
- .filter(
1439
- tables.DjmdPlaylist.ParentID == playlist.ParentID,
1440
- old_seq < tables.DjmdPlaylist.Seq,
1441
- tables.DjmdPlaylist.Seq <= seq,
1442
- )
1443
- .order_by(tables.DjmdPlaylist.Seq)
1444
- )
1445
- other_playlists = query.all()
1446
- delta_seq = -1
1447
- elif seq < old_seq:
1448
- query = (
1449
- self.query(tables.DjmdPlaylist)
1450
- .filter(
1451
- tables.DjmdPlaylist.ParentID == playlist.ParentID,
1452
- seq <= tables.DjmdPlaylist.Seq,
1453
- tables.DjmdPlaylist.Seq < old_seq,
1454
- )
1455
- .order_by(tables.DjmdPlaylist.Seq)
1456
- )
1457
- other_playlists = query.all()
1458
- delta_seq = +1
1459
- else:
1460
- return
1461
-
1462
- # Set seq number and update time *before* other playlists to ensure
1463
- # right USN increment order
1464
- playlist.Seq = seq
1465
- # Each move counts as one USN increment, so disable for update time
1466
- self.registry.disable_tracking()
1467
- playlist.updated_at = now
1468
- self.registry.enable_tracking()
1469
-
1470
- # Set seq number and update time for playlists between old_seq and seq
1471
- for pl in other_playlists:
1472
- pl.Seq += delta_seq
1473
- # Each move counts as one USN increment, so disable for update time
1474
- self.registry.disable_tracking()
1475
- pl.updated_at = now
1476
- self.registry.enable_tracking()
1477
-
1478
- def rename_playlist(self, playlist, name):
1479
- """Renames a playlist or playlist folder.
1480
-
1481
- Parameters
1482
- ----------
1483
- playlist : DjmdPlaylist or int or str
1484
- The playlist or playlist folder to move. Can either be a
1485
- :class:`DjmdPlaylist` object or a playlist ID.
1486
- name : str
1487
- The new name of the playlist or playlist folder.
1488
-
1489
- Examples
1490
- --------
1491
- Take the following playlist tree:
1492
-
1493
- >>> db = Rekordbox6Database()
1494
- >>> playlists = db.get_playlist().order_by(tables.DjmdPlaylist.Seq)
1495
- >>> [pl.Name for pl in playlists] # noqa
1496
- ['Playlist 1', 'Playlist 2']
1497
-
1498
- Rename a playlist:
1499
-
1500
- >>> pl = db.get_playlist(Name="Playlist 1").one() # noqa
1501
- >>> db.rename_playlist(pl, name="Playlist new")
1502
- >>> playlists = db.get_playlist().order_by(tables.DjmdPlaylist.Seq)
1503
- >>> [pl.Name for pl in playlists] # noqa
1504
- ['Playlist new', 'Playlist 2']
1505
- """
1506
- if isinstance(playlist, (int, str)):
1507
- playlist = self.get_playlist(ID=playlist)
1508
- now = datetime.datetime.now()
1509
- # Update name of playlist
1510
- playlist.Name = name
1511
- # Update update time: USN not incremented
1512
- self.registry.disable_tracking()
1513
- playlist.updated_at = now
1514
- self.registry.enable_tracking()
1515
-
1516
- # ----------------------------------------------------------------------------------
1517
-
1518
- def get_mysetting_paths(self):
1519
- """Returns the file paths of the local Rekordbox MySetting files.
1520
-
1521
- Returns
1522
- -------
1523
- paths : list[str]
1524
- the file paths of the local MySetting files.
1525
- """
1526
- paths = list()
1527
- for item in self.get_setting_file():
1528
- paths.append(self._db_dir / item.Path.lstrip("/\\"))
1529
- return paths
1530
-
1531
- def get_anlz_dir(self, content):
1532
- """Returns the directory path containing the ANLZ analysis files of a track.
1533
-
1534
- Parameters
1535
- ----------
1536
- content : DjmdContent or int or str
1537
- The content corresponding to a track in the Rekordbox v6 database.
1538
- If an integer is passed the database is queried for the ``DjmdContent``
1539
- entry.
1540
-
1541
- Returns
1542
- -------
1543
- anlz_dir : Path
1544
- The path of the directory containing the analysis files for the content.
1545
- """
1546
- if isinstance(content, (int, str)):
1547
- content = self.get_content(ID=content)
1548
-
1549
- dat_path = Path(content.AnalysisDataPath.strip("\\/"))
1550
- path = self._share_dir / dat_path.parent
1551
- return path
1552
-
1553
- def get_anlz_paths(self, content):
1554
- """Returns all existing ANLZ analysis file paths of a track.
1555
-
1556
- Parameters
1557
- ----------
1558
- content : DjmdContent or int or str
1559
- The content corresponding to a track in the Rekordbox v6 database.
1560
- If an integer is passed the database is queried for the ``DjmdContent``
1561
- entry.
1562
-
1563
- Returns
1564
- -------
1565
- anlz_paths : dict[str, Path]
1566
- The analysis file paths for the content as dictionary. The keys of the
1567
- dictionary are the file types ("DAT", "EXT" or "EX2").
1568
- """
1569
- root = self.get_anlz_dir(content)
1570
- return get_anlz_paths(root)
1571
-
1572
- def read_anlz_files(self, content):
1573
- """Reads all existing ANLZ analysis files of a track.
1574
-
1575
- Parameters
1576
- ----------
1577
- content : DjmdContent or int or str
1578
- The content corresponding to a track in the Rekordbox v6 database.
1579
- If an integer is passed the database is queried for the ``DjmdContent``
1580
- entry.
1581
-
1582
- Returns
1583
- -------
1584
- anlz_files : dict[str, AnlzFile]
1585
- The analysis files for the content as dictionary. The keys of the
1586
- dictionary are the file paths.
1587
- """
1588
- root = self.get_anlz_dir(content)
1589
- return read_anlz_files(root)
1590
-
1591
- def update_content_path(self, content, path, save=True, check_path=True):
1592
- """Update the file path of a track in the Rekordbox v6 database.
1593
-
1594
- This changes the `FolderPath` entry in the ``DjmdContent`` table and the
1595
- path tag (PPTH) of the corresponding ANLZ analysis files.
1596
-
1597
- Parameters
1598
- ----------
1599
- content : DjmdContent or int or str
1600
- The ``DjmdContent`` element to change. If an integer is passed the database
1601
- is queried for the content.
1602
- path : str or Path
1603
- The new file path of the database entry.
1604
- save : bool, optional
1605
- If True, the changes made are written to disc.
1606
- check_path : bool, optional
1607
- If True, raise an assertion error if the given file path does not exist.
1608
-
1609
- Examples
1610
- --------
1611
- If, for example, the file `NOISE.wav` was moved up a few directories
1612
- (from `.../Sampler/OSC_SAMPLER/PRESET ONESHOT/` to `.../Sampler/`) the file
1613
- could no longer be opened in Rekordbox, since the database still contains the
1614
- old file path:
1615
-
1616
- >>> db = Rekordbox6Database()
1617
- >>> cont = db.get_content()[0]
1618
- >>> cont.FolderPath
1619
- C:/Music/PioneerDJ/Sampler/OSC_SAMPLER/PRESET ONESHOT/NOISE.wav
1620
-
1621
- Updating the path changes the database entry
1622
-
1623
- >>> new_path = "C:/Music/PioneerDJ/Sampler/PRESET ONESHOT/NOISE.wav"
1624
- >>> db.update_content_path(cont, new_path)
1625
- >>> cont.FolderPath
1626
- C:/Music/PioneerDJ/Sampler/PRESET ONESHOT/NOISE.wav
1627
-
1628
- and updates the file path in the corresponding ANLZ analysis files:
1629
-
1630
- >>> files = self.read_anlz_files(cont.ID)
1631
- >>> file = list(files.values())[0]
1632
- >>> file.get("path")
1633
- C:/Music/PioneerDJ/Sampler/PRESET ONESHOT/NOISE.wav
1634
-
1635
- """
1636
- if isinstance(content, (int, str)):
1637
- content = self.get_content(ID=content)
1638
- cid = content.ID
1639
-
1640
- path = Path(path)
1641
- # Check and format path (the database and ANLZ files use "/" as path delimiter)
1642
- if check_path:
1643
- assert path.exists()
1644
- path = str(path).replace("\\", "/")
1645
- old_path = content.FolderPath
1646
- logger.info("Replacing '%s' with '%s' of content [%s]", old_path, path, cid)
1647
-
1648
- # Update path in ANLZ files
1649
- anlz_files = self.read_anlz_files(cid)
1650
- for anlz_path, anlz in anlz_files.items():
1651
- logger.debug("Updating path of %s: %s", anlz_path, path)
1652
- anlz.set_path(path)
1653
-
1654
- # Update path in database (DjmdContent)
1655
- logger.debug("Updating database file path: %s", path)
1656
- content.FolderPath = path
1657
-
1658
- if save:
1659
- logger.debug("Saving changes")
1660
- # Save ANLZ files
1661
- for anlz_path, anlz in anlz_files.items():
1662
- anlz.save(anlz_path)
1663
- # Commit database changes
1664
- self.commit()
1665
-
1666
- def update_content_filename(self, content, name, save=True, check_path=True):
1667
- """Update the file name of a track in the Rekordbox v6 database.
1668
-
1669
- This changes the `FolderPath` entry in the ``DjmdContent`` table and the
1670
- path tag (PPTH) of the corresponding ANLZ analysis files.
1671
-
1672
- Parameters
1673
- ----------
1674
- content : DjmdContent or int or str
1675
- The ``DjmdContent`` element to change. If an integer is passed the database
1676
- is queried for the content.
1677
- name : str
1678
- The new file name of the database entry.
1679
- save : bool, optional
1680
- If True, the changes made are written to disc.
1681
- check_path : bool, optional
1682
- If True, raise an assertion error if the new file path does not exist.
1683
-
1684
- See Also
1685
- --------
1686
- update_content_path: Update the file path of a track in the Rekordbox database.
1687
-
1688
- Examples
1689
- --------
1690
- Updating the file name changes the database entry
1691
-
1692
- >>> db = Rekordbox6Database()
1693
- >>> cont = db.get_content()[0]
1694
- >>> cont.FolderPath
1695
- C:/Music/PioneerDJ/Sampler/OSC_SAMPLER/PRESET ONESHOT/NOISE.wav
1696
-
1697
- >>> new_name = "noise"
1698
- >>> db.update_content_filename(cont, new_name)
1699
- >>> cont.FolderPath
1700
- C:/Music/PioneerDJ/Sampler/OSC_SAMPLER/PRESET ONESHOT/noise.wav
1701
-
1702
- and updates the file path in the corresponding ANLZ analysis files:
1703
-
1704
- >>> files = self.read_anlz_files(cont.ID)
1705
- >>> file = list(files.values())[0]
1706
- >>> cont.FolderPath == file.get("path")
1707
- True
1708
-
1709
- """
1710
- if isinstance(content, (int, str)):
1711
- content = self.get_content(ID=content)
1712
-
1713
- old_path = Path(content.FolderPath)
1714
- ext = old_path.suffix
1715
- new_path = old_path.parent / name
1716
- new_path = new_path.with_suffix(ext)
1717
- self.update_content_path(content, new_path, save, check_path)
1718
-
1719
- def to_dict(self, verbose=False):
1720
- """Convert the database to a dictionary.
1721
-
1722
- Parameters
1723
- ----------
1724
- verbose: bool, optional
1725
- If True, print the name of the table that is currently converted.
1726
-
1727
- Returns
1728
- -------
1729
- dict
1730
- A dictionary containing the database tables as keys and the table data as
1731
- a list of dicts.
1732
- """
1733
- data = dict()
1734
- for table_name in tables.__all__:
1735
- if table_name.startswith("Stats") or table_name == "Base":
1736
- continue
1737
- if verbose:
1738
- print(f"Converting table: {table_name}")
1739
- table = getattr(tables, table_name)
1740
- columns = table.columns()
1741
- table_data = list()
1742
- for row in self.query(table).all():
1743
- table_data.append({column: row[column] for column in columns})
1744
- data[table_name] = table_data
1745
- return data
1746
-
1747
- def to_json(self, file, indent=4, sort_keys=True, verbose=False):
1748
- """Convert the database to a JSON file."""
1749
- import json
1750
-
1751
- def json_serial(obj):
1752
- if isinstance(obj, (datetime.datetime, datetime.date)):
1753
- return obj.isoformat()
1754
- raise TypeError(f"Type {type(obj)} not serializable")
1755
-
1756
- data = self.to_dict(verbose=verbose)
1757
- with open(file, "w") as fp:
1758
- json.dump(data, fp, indent=indent, sort_keys=sort_keys, default=json_serial)
1759
-
1760
- def copy_unlocked(self, output_file):
1761
- src_engine = self.engine
1762
- src_metadata = MetaData()
1763
- exclude_tables = ("sqlite_master", "sqlite_sequence", "sqlite_temp_master")
1764
-
1765
- dst_engine = create_engine(f"sqlite:///{output_file}")
1766
- dst_metadata = MetaData()
1767
-
1768
- @event.listens_for(src_metadata, "column_reflect")
1769
- def genericize_datatypes(inspector, tablename, column_dict):
1770
- type_ = column_dict["type"].as_generic(allow_nulltype=True)
1771
- if isinstance(type_, DateTime):
1772
- type_ = String
1773
- column_dict["type"] = type_
1774
-
1775
- src_conn = src_engine.connect()
1776
- dst_conn = dst_engine.connect()
1777
- dst_metadata.reflect(bind=dst_engine)
1778
- # drop all tables in target database
1779
- for table in reversed(dst_metadata.sorted_tables):
1780
- if table.name not in exclude_tables:
1781
- print("dropping table =", table.name)
1782
- table.drop(bind=dst_engine)
1783
- # Delete all data in target database
1784
- for table in reversed(dst_metadata.sorted_tables):
1785
- table.delete()
1786
- dst_metadata.clear()
1787
- dst_metadata.reflect(bind=dst_engine)
1788
- src_metadata.reflect(bind=src_engine)
1789
- # create all tables in target database
1790
- for table in src_metadata.sorted_tables:
1791
- if table.name not in exclude_tables:
1792
- table.create(bind=dst_engine)
1793
- # refresh metadata before you can copy data
1794
- dst_metadata.clear()
1795
- dst_metadata.reflect(bind=dst_engine)
1796
- # Copy all data from src to target
1797
- print("Copying data...")
1798
- string = "\rCopying table {name}: Inserting row {row}"
1799
- index = 0
1800
- for table in dst_metadata.sorted_tables:
1801
- src_table = src_metadata.tables[table.name]
1802
- stmt = table.insert()
1803
- for index, row in enumerate(src_conn.execute(src_table.select())):
1804
- print(string.format(name=table.name, row=index), end="", flush=True)
1805
- dst_conn.execute(stmt.values(row))
1806
- print(f"\rCopying table {table.name}: Inserted {index} rows", flush=True)
1807
-
1808
- dst_conn.commit()