Metadata-Version: 2.4
Name: serato-tools
Version: 1.15.0
Summary: Serato library and track metadata modification
Author-email: bvandercar-vt <bvandercar@outlook.com>
License: Copyright 2019 Jan Holthuis
        
        Permission is hereby granted, free of charge, to any person obtaining a copy of
        this software and associated documentation files (the "Software"), to deal in
        the Software without restriction, including without limitation the rights to
        use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
        of the Software, and to permit persons to whom the Software is furnished to do
        so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mutagen
Provides-Extra: waveform-drawing
Requires-Dist: pillow; extra == "waveform-drawing"
Provides-Extra: beatgrid-analysis
Requires-Dist: librosa; extra == "beatgrid-analysis"
Requires-Dist: numpy; extra == "beatgrid-analysis"
Dynamic: license-file

Includes:
- Serato file GEOB tag parsing and modification (from https://github.com/Holzhaus/serato-tags , which appears to be no longer maintained)
- Serato overall database parsing and modification
- Serato Crate parsing and modification (from https://github.com/sharst/seratopy)
- Dynamic beatgrid analysis and saving to Serato tags (beatgrid analysis from https://github.com/heyitsmass/audio/blob/master/audio/beat_grid.py)

# Serato Tags

Original writeup on Serato GEOB tag discoveries: [blog post](https://homepage.ruhr-uni-bochum.de/jan.holthuis/posts/reversing-seratos-geob-tags)

| Tag                                          | Progress      | Contents                   | Example script                               |
| -------------------------------------------- | ------------- | -------------------------- | -------------------------------------------- |
| [`Serato Analysis`](docs/serato_analysis.md) | Done          | Serato version information |
| [`Serato Autotags`](docs/serato_autotags.md) | Done          | BPM and Gain values        | [`track_autotags.py`](src/track_autotags.py) |
| [`Serato BeatGrid`](docs/serato_beatgrid.md) | Mostly done   | Beatgrid Markers           | [`track_beatgrid.py`](src/track_beatgrid.py) |
| [`Serato Markers2`](docs/serato_markers2.md) | Mostly done   | Hotcues, Saved Loops, etc. | [`track_cues_v2.py`](src/track_cues_v2.py)   |
| [`Serato Markers_`](docs/serato_markers_.md) | Mostly done   | Hotcues, Saved Loops, etc. | [`track_cues_v1.py`](src/track_cues_v1.py)   |
| [`Serato Offsets_`](docs/serato_offsets_.md) | _Not started_ |                            |
| [`Serato Overview`](docs/serato_overview.md) | Done          | Waveform data              | [`track_waveform.py`](src/track_waveform.py) |

The different file/tag formats that Serato uses to store the information are documented in [`docs/fileformats.md`](docs/fileformats.md), a script to dump the tag data can be found at [`track_tagdump.py`](src/track_tagdump.py).

# Examples

### Analyzing and setting a dynamic beatgrid

```cmd
>>> analyze_beatgrid "Music/Techno/Adam Beyer - Space Date (Pleasurekraft Remix).mp3"
```

### Modifying the database file

```python
from serato_tools.database_v2 import DatabaseV2

db = DatabaseV2()

now = int(time.time())

def modify_uadd(filename: str, value: Any):
    print(f'Serato library change - Changed "date added" to today: {filename}')
    return now

def modify_tadd(filename: str, value: Any):
    return str(now)

def remove_group(filename: str, value: Any):
    return " "

# a list of field keys can be found in serato_tools.database_v2
db.modify_file(
    rules=[
        {"field": "uadd", "files": files_set_date, "func": modify_uadd},
        {"field": "tadd", "files": files_set_date, "func": modify_tadd},
        {"field": "tgrp", "func": remove_group}, # all files
    ]
)
```

### Setting track color

Must delete delete_tags_v1 in order to take affect in Serato. Not sure what tags v1 is even for, probably older versions of Serato. I have found no issues with deleting this. But be warned, in case you are running an older version.

```python
from serato_tools.track_cues_v2 import TRACK_COLORS, set_track_color

set_track_color('/Users/Username/Music/Techno/T78 - Acid Lick.mp3', TRACK_COLORS["purple"], print_changes=True, delete_tags_v1=True)

```

### Modifying track metadata / hot cues

Must delete delete_tags_v1 in order to take affect in Serato. Not sure what tags v1 is even for, probably older versions of Serato. I have found no issues with deleting this. But be warned, in case you are running an older version.

```python
from mutagen.mp3 import MP3
from mutagen.id3._frames import TIT1

import serato_tools.track_cues_v1
from serato_tools.track_cues_v2 import CUE_COLORS, TRACK_COLORS, ValueType, modify_file_entries
from serato_tools.utils.tags import del_geob

tagfile = MP3(file)

def red_fix(value: ValueType):
    if value in [CUE_COLORS["pinkred"], CUE_COLORS["magenta"]]:
        print("Cue changed to red")
        del_geob(tagfile, serato_tools.track_cues_v1.GEOB_KEY) # delete serato_markers, not sure if this field even takes effect in new versions of Serato, we just want serato_markers2
        return CUE_COLORS["red"]

def name_changes(value: ValueType):
    if (not isinstance(value, str)) or value == "":
        return

    # make cue names all caps
    value_caps = value.strip().upper()
    if value != value_caps:
        return value_caps

def set_grouping_based_on_track_color(value: ValueType):
    if value == TRACK_COLORS["limegreen3"]:
        tagfile.tags.setall("TIT1", [TIT1(text="TAGGED")])
    elif value in [ TRACK_COLORS["white"], TRACK_COLORS["grey"], TRACK_COLORS["black"]]:
        tagfile.tags.setall("TIT1", [TIT1(text="UNTAGGED")])

modify_file_entries(
    tagfile,
    {
        "cues": [
            {"field": "color", "func": red_fix},
            {"field": "name", "func": name_changes},
        ],
        "color": [
            {"field": "color", "func": set_grouping_based_on_track_color},
        ],
    },
    print_changes=True,
    delete_tags_v1=True
)
```

### Crate details and adding a track

```python
from serato_tools.crate import Crate

crate = Crate('/Users/Username/Music/_Serato_/Subcrates/Techno.crate')

print(crate)
# OUTPUT:
#
# Crate containing 81 tracks:
# Music/Techno/Adam Beyer - Space Date (Pleasurekraft Remix).mp3
# Music/Techno/Adam Beyer - Your Mind (Will Clarke Remix).mp3
# Music/Techno/Adam Beyer - Your Mind.mp3
# Music/Techno/Alberto Ruiz - Expressor (Hell Driver Remix).mp3
# ...

crate.print_data()
# OUTPUT:
#
# [   ('vrsn', '1.0/Serato ScratchLive Crate'),
#     ('osrt', [('brev', b'\x00')]),
#     ('ovct', [('tvcn', 'key'), ('tvcw', '0')]),
#     ('ovct', [('tvcn', 'artist'), ('tvcw', '0')]),
#     ('ovct', [('tvcn', 'song'), ('tvcw', '0')]),
#     ('ovct', [('tvcn', 'bpm'), ('tvcw', '0')]),
#     ('ovct', [('tvcn', 'playCount'), ('tvcw', '0')]),
#     ('ovct', [('tvcn', 'length'), ('tvcw', '0')]),
#     ('ovct', [('tvcn', 'added'), ('tvcw', '0')]),
#     (   'otrk',
#         [   (   'ptrk',
#                 'Music/Techno/Adam Beyer - Space Date (Pleasurekraft Remix).mp3')]),
#     (   'otrk',
#         [   (   'ptrk',
#                 'Music/Techno/Adam Beyer - Your Mind (Will Clarke Remix).mp3')]),
#     ('otrk', [('ptrk', 'Music/Techno/Adam Beyer - Your Mind.mp3')]),
# ...


# Example: Add a track to the crate and save it as a new crate
crate.add_track('/Users/Username/Music/Techno/T78 - Acid Lick.mp3')
crate.save_to_file('/Users/Username/Music/Techno/New Crate.crate')
```
